From b66609d54a59afcf154d9234d9ab558138469f76 Mon Sep 17 00:00:00 2001 From: Mark Watney <80194956+markonmars@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:16:51 +0800 Subject: [PATCH] Add github workflows (#125) * Add GitHub workflows (#119) * fix broken tests * general tidying * add github workflow * correct extension * correct name * format * tidy * Remove `refund` action for testnet competition (#120) * remove refund all action for testnet * fix script * Fix workflow (#121) * run on PR * fix tests * format * Mp 2736 improve estimates (#122) * use wasms * fmt * tidy * add secret manager * config update * remove wasm files * Update mars endpoints. * improve logging * fix tests * fmt --------- Co-authored-by: piobab * update secret manager (#123) * Update contract addresses (#124) * update secret manager * update addresses * fmt --------- Co-authored-by: piobab --- .github/workflows/scripts.yml | 41 ++ package.json | 10 +- src/BaseExecutor.ts | 205 ++++----- src/helpers.ts | 8 +- src/main.ts | 58 ++- src/query/chainQuery.ts | 139 ++++++ src/query/contractQuery.ts | 81 ---- src/query/hive.ts | 175 -------- src/query/routing/OsmosisRouteRequester.ts | 4 +- src/query/types.ts | 63 +-- src/redbank/LiquidationHelpers.ts | 1 - src/redbank/RedbankExecutor.ts | 405 ++++++++---------- src/redbank/config/neutron.ts | 43 +- src/redbank/config/osmosis.ts | 44 +- src/rover/ActionGenerator.ts | 140 +++--- src/rover/RoverExecutor.ts | 281 ++++++------ src/rover/config/neutron.ts | 74 ++-- src/rover/config/osmosis.ts | 73 ++-- src/rover/types/MarketInfo.ts | 4 +- src/secretManager.ts | 32 +- test/liquidationGenerator.test.ts | 6 +- test/query/contractQuery.test.ts | 12 +- test/redbank/unit/redbankExecutor.test.ts | 14 +- test/rover/mocks/stateMock.ts | 120 +++++- .../unit/LiquidationActionGenerator.test.ts | 56 ++- test/rover/unit/executor.test.ts | 15 +- yarn.lock | 10 + 27 files changed, 1067 insertions(+), 1047 deletions(-) create mode 100644 .github/workflows/scripts.yml create mode 100644 src/query/chainQuery.ts delete mode 100644 src/query/contractQuery.ts delete mode 100644 src/query/hive.ts diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml new file mode 100644 index 0000000..9093941 --- /dev/null +++ b/.github/workflows/scripts.yml @@ -0,0 +1,41 @@ +name: Scripts + +on: + push: + branches: + - main + - perps + pull_request: + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Required for coverage comparison + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' # Specify yarn for caching + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: Run Tests + run: yarn test + + - name: Check Coverage + run: yarn test:coverage + + - name: Check Formatting + run: yarn format:check diff --git a/package.json b/package.json index 25e6f5a..fd05f61 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "scripts": { "test:integration": "yarn build && node build/test/redbank/integration/liquidationTest", "test:rover-integration": "yarn build && node build/test/rover/integration/liquidationTest", + "clean": "rimraf build", "build": "tsc", "start": "yarn build && node build/src/main", - "test": "jest", + "test": "yarn clean && yarn build && jest", + "test:coverage": "yarn clean && yarn build && jest --coverage", "lint": "yarn format-check && eslint . && yarn build", "format": "prettier --write .", - "format-check": "prettier --ignore-path .gitignore --check ." + "format:check": "prettier --ignore-path .gitignore --check ." }, "dependencies": { "@aws-sdk/client-secrets-manager": "^3.188.0", @@ -40,7 +42,9 @@ "requests": "^0.3.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "mars-rover-health-computer-node": "2.2.0", + "mars-liquidation-node": "1.0.0" }, "devDependencies": { "@babel/core": "^7.21.0", diff --git a/src/BaseExecutor.ts b/src/BaseExecutor.ts index 7bfea38..be927f5 100644 --- a/src/BaseExecutor.ts +++ b/src/BaseExecutor.ts @@ -1,6 +1,5 @@ import { SigningStargateClient, StdFee } from '@cosmjs/stargate' import { Coin, EncodeObject, coins } from '@cosmjs/proto-signing' -import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { AMMRouter } from './AmmRouter' import { ConcentratedLiquidityPool, Pool, PoolType, XYKPool } from './types/Pool' import 'dotenv/config.js' @@ -8,22 +7,20 @@ import { MarketInfo } from './rover/types/MarketInfo' import { CSVWriter, Row } from './CsvWriter' import BigNumber from 'bignumber.js' -import { fetchRedbankData } from './query/hive' import { RouteRequester } from './query/routing/RouteRequesterInterface' import { sleep } from './helpers' -import { OraclePriceFetcher as MarsOraclePriceFetcher } from './query/oracle/OraclePriceFetcher' -import { PythPriceFetcher } from './query/oracle/PythPriceFetcher' -import { OraclePrice } from './query/oracle/PriceFetcherInterface' import { PriceSourceResponse } from './types/oracle' +import { AssetParamsBaseForAddr, PerpParams } from 'marsjs-types/mars-params/MarsParams.types' +import { ChainQuery } from './query/chainQuery' +import { PriceResponse } from 'marsjs-types/mars-oracle-osmosis/MarsOracleOsmosis.types' +import { Market } from 'marsjs-types/mars-red-bank/MarsRedBank.types' +import { Dictionary } from 'lodash' export interface BaseConfig { lcdEndpoint: string chainName: string productName: string - hiveEndpoint: string - oracleAddress: string - redbankAddress: string - marsParamsAddress: string + contracts: Dictionary liquidatorMasterAddress: string gasDenom: string neutralAssetDenom: string @@ -44,15 +41,12 @@ export interface BaseConfig { export class BaseExecutor { private priceSources: PriceSourceResponse[] = [] - private marsOraclePriceFetcher: MarsOraclePriceFetcher = new MarsOraclePriceFetcher( - this.queryClient, - ) - private pythOraclePriceFetcher: PythPriceFetcher = new PythPriceFetcher() - // Data public prices: Map = new Map() public balances: Map = new Map() public markets: Map = new Map() + public assetParams: Map = new Map() + public perpParams: Map = new Map() // logging private csvLogger = new CSVWriter('./results.csv', [ @@ -66,15 +60,15 @@ export class BaseExecutor { constructor( public config: BaseConfig, - public client: SigningStargateClient, - public queryClient: CosmWasmClient, + public signingClient: SigningStargateClient, + public queryClient: ChainQuery, public routeRequester: RouteRequester, public ammRouter: AMMRouter = new AMMRouter(), ) { console.log({ config }) } - applyAvailableLiquidity = (market: MarketInfo): MarketInfo => { + applyAvailableLiquidity = (market: Market): MarketInfo => { // Available liquidity = deposits - borrows. However, we need to // compute the underlying uasset amounts from the scaled amounts. const scalingFactor = 1e6 @@ -88,12 +82,16 @@ export class BaseExecutor { const availableLiquidity = descaledDeposits.minus(descaledBorrows) - market.available_liquidity = availableLiquidity - return market + const marketInfo = { + available_liquidity: availableLiquidity, + ...market, + } + + return marketInfo } setBalances = async (liquidatorAddress: string) => { - const coinBalances: readonly Coin[] = await this.client.getAllBalances(liquidatorAddress) + const coinBalances: readonly Coin[] = await this.signingClient.getAllBalances(liquidatorAddress) for (const index in coinBalances) { const coin = coinBalances[index] this.balances.set(coin.denom, Number(coin.amount)) @@ -108,20 +106,19 @@ export class BaseExecutor { await this.csvLogger.writeToFile() } - refreshData = async () => { + init = async () => { // dispatch hive request and parse it - const { bank } = await fetchRedbankData( - this.config.hiveEndpoint, - this.config.liquidatorMasterAddress, - this.config.redbankAddress, - ) - - bank.balance.forEach((coin) => this.balances.set(coin.denom, Number(coin.amount))) + await this.updatePriceSources() + await this.updateOraclePrices() + await this.updatePrices() + await this.updateMarketsData() + await this.updateAssetParams() + } + updatePrices = async () => { await this.updatePriceSources() await this.updateOraclePrices() - await this.refreshMarketData() } updatePriceSources = async () => { @@ -135,12 +132,7 @@ export class BaseExecutor { while (fetching) { try { - const response = await this.queryClient.queryContractSmart(this.config.oracleAddress, { - price_sources: { - limit, - start_after, - }, - }) + const response = await this.queryClient.queryOraclePriceSources(start_after, limit) start_after = response[response.length - 1] ? response[response.length - 1].denom : '' priceSources = priceSources.concat(response) fetching = response.length === limit @@ -164,12 +156,80 @@ export class BaseExecutor { } } + updatePerpParams = async () => { + let perpParams: PerpParams[] = [] + let fetching = true + let start_after = '' + let retries = 0 + + const maxRetries = 5 + const limit = 5 + + while (fetching) { + try { + const response = await this.queryClient.queryPerpParams(start_after, limit) + start_after = response[response.length - 1] ? response[response.length - 1].denom : '' + perpParams = perpParams.concat(response) + fetching = response.length === limit + retries = 0 + } catch (e) { + console.warn(e) + retries++ + if (retries >= maxRetries) { + console.warn('Max retries exceeded, exiting', maxRetries) + fetching = false + } else { + await sleep(5000) + console.info('Retrying...') + } + } + } + + // don't override if we did not fetch all data. + if (retries < maxRetries) { + perpParams.forEach((perpParam) => { + this.perpParams.set(perpParam.denom, perpParam) + }) + } + } + + updateAssetParams = async () => { + const maxRetries = 5 + const limit = 5 + + // while not returning empty, get all asset params + let fetching = true + let startAfter = '' + let retries = 0 + while (fetching) { + try { + const response = await this.queryClient.queryAllAssetParams(startAfter, limit) + startAfter = response[response.length - 1] ? response[response.length - 1].denom : '' + response.forEach((assetParam: AssetParamsBaseForAddr) => { + this.assetParams.set(assetParam.denom, assetParam) + }) + fetching = response.length === 5 + retries = 0 + } catch (ex) { + console.warn(ex) + retries++ + if (retries > maxRetries) { + console.warn('Max retries exceeded, exiting', maxRetries) + fetching = false + } else { + await sleep(5000) + console.info('Retrying...') + } + } + } + } + updateOraclePrices = async () => { try { // Fetch all price sources - const priceResults: PromiseSettledResult[] = await Promise.allSettled( + const priceResults: PromiseSettledResult[] = await Promise.allSettled( this.priceSources.map( - async (priceSource) => await this.fetchOraclePrice(priceSource.denom), + async (priceSource) => await this.queryClient.queryOraclePrice(priceSource.denom), ), ) @@ -179,7 +239,7 @@ export class BaseExecutor { // push successfull price results if (successfull && oraclePrice) { - this.prices.set(oraclePrice.denom, oraclePrice.price) + this.prices.set(oraclePrice.denom, new BigNumber(oraclePrice.price)) } }) } catch (e) { @@ -187,68 +247,21 @@ export class BaseExecutor { } } - private fetchOraclePrice = async (denom: string): Promise => { - const priceSource: PriceSourceResponse | undefined = this.priceSources.find( - (ps) => ps.denom === denom, - ) - if (!priceSource) { - console.error(`No price source found for ${denom}`) - } - - switch (priceSource?.price_source!.toString()) { - case 'fixed': - case 'spot': - // todo - support via pool query. These will default to oracle price - case 'arithmetic_twap': - case 'geometric_twap': - case 'xyk_liquidity_token': - case 'lsd': - case 'staked_geometric_twap': - return await this.marsOraclePriceFetcher.fetchPrice({ - oracleAddress: this.config.oracleAddress, - priceDenom: denom, - }) - case 'pyth': - const pyth: { - price_feed_id: string - denom_decimals: number - //@ts-expect-error - our generated types don't handle this case - } = priceSource.price_source.pyth - - return await this.pythOraclePriceFetcher.fetchPrice({ - priceFeedId: pyth.price_feed_id, - denomDecimals: pyth.denom_decimals, - denom: denom, - }) - // Handle other cases for different price source types - default: - // Handle unknown or unsupported price source types - return await this.marsOraclePriceFetcher.fetchPrice({ - oracleAddress: this.config.oracleAddress, - priceDenom: denom, - }) - } - } - - refreshMarketData = async () => { - let markets: MarketInfo[] = [] + updateMarketsData = async () => { + let markets: Market[] = [] let fetching = true + let limit = 5 let start_after = '' while (fetching) { - const response = await this.queryClient.queryContractSmart(this.config.redbankAddress, { - markets: { - start_after, - limit: 5, - }, - }) - + const response = await this.queryClient.queryMarkets(start_after, limit) start_after = response[response.length - 1] ? response[response.length - 1].denom : '' markets = markets.concat(response) fetching = response.length === 5 } - // @ts-ignore TODO - this.markets = markets.map((market: MarketInfo) => this.applyAvailableLiquidity(market)) + this.markets = new Map( + markets.map((market) => [market.denom, this.applyAvailableLiquidity(market)]), + ) } // Filter out pools that are invalid @@ -294,7 +307,7 @@ export class BaseExecutor { // Calculate the fee for a transaction on osmosis. Incorporates EIP1559 dynamic base fee getOsmosisFee = async (msgs: EncodeObject[], address: string) => { - if (!this.client) + if (!this.signingClient) throw new Error( 'Stargate Client is undefined, ensure you call initiate at before calling this method', ) @@ -302,7 +315,7 @@ export class BaseExecutor { `${process.env.LCD_ENDPOINT}/osmosis/txfees/v1beta1/cur_eip_base_fee?x-apikey=${process.env.API_KEY}`, ) const { base_fee: baseFee } = await gasPriceRequest.json() - const gasEstimated = await this.client.simulate(address, msgs, '') + const gasEstimated = await this.signingClient.simulate(address, msgs, '') const gas = Number(gasEstimated * 1.3) const gasPrice = Number(baseFee) const safeGasPrice = gasPrice < 0.025 ? 0.025 : gasPrice @@ -316,7 +329,7 @@ export class BaseExecutor { } getNeutronFee = async (msgs: EncodeObject[], address: string): Promise => { - if (!this.client) + if (!this.signingClient) throw new Error( 'Stargate Client is undefined, ensure you call initiate at before calling this method', ) @@ -325,7 +338,7 @@ export class BaseExecutor { await gasPriceRequest.json() const baseFee = fees.params.minimum_gas_prices.filter((price) => price.denom === 'untrn')[0] //todo default here const gasPrice = Number(baseFee.amount) - const gasEstimated = await this.client.simulate(address, msgs, '') + const gasEstimated = await this.signingClient.simulate(address, msgs, '') const gas = Number(gasEstimated * 1.8) const safeGasPrice = gasPrice < 0.025 ? 0.025 : gasPrice const amount = coins((gas * safeGasPrice + 1).toFixed(0), this.config.gasDenom) diff --git a/src/helpers.ts b/src/helpers.ts index bba149b..1cbe08a 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -402,7 +402,7 @@ export const calculatePositionStateAfterPerpClosure = ( // if we still have debt, we need to increment it if (baseDenomDebts) { baseDenomDebts.amount = debtAmount.plus(remainingDebt).toString() - } else { + } else if (remainingDebt.gt(0)) { positions.debts.push({ amount: remainingDebt.toString(), denom: baseDenom, @@ -418,11 +418,15 @@ export const calculatePositionStateAfterPerpClosure = ( positions.deposits.push({ amount: totalPerpPnl.abs().toString(), denom: baseDenom, - shares: totalPerpPnl.abs().toString(), // do we care about shares? + shares: totalPerpPnl.abs().toString(), }) } } + positions.debts = positions.debts.filter((debt) => BigNumber(debt.amount).gt(0)) + positions.deposits = positions.deposits.filter((deposit) => BigNumber(deposit.amount).gt(0)) + positions.lends = positions.lends.filter((lend) => BigNumber(lend.amount).gt(0)) + return positions } diff --git a/src/main.ts b/src/main.ts index c27148a..ab55419 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,7 @@ -import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { HdPath } from '@cosmjs/crypto' import { DirectSecp256k1HdWallet, makeCosmoshubPath } from '@cosmjs/proto-signing' import { SigningStargateClient } from '@cosmjs/stargate' -import { produceReadOnlyCosmWasmClient, produceSigningStargateClient } from './helpers.js' +import { produceSigningStargateClient } from './helpers.js' import { RedbankExecutor, RedbankExecutorConfig } from './redbank/RedbankExecutor.js' import { RoverExecutor, RoverExecutorConfig } from './rover/RoverExecutor.js' import { getSecretManager } from './secretManager.js' @@ -15,6 +14,7 @@ import { AstroportCW } from './exchange/Astroport.js' import { AstroportRouteRequester } from './query/routing/AstroportRouteRequester.js' import { OsmosisRouteRequester } from './query/routing/OsmosisRouteRequester.js' import { RouteRequester } from './query/routing/RouteRequesterInterface.js' +import { ChainQuery } from './query/chainQuery.js' const REDBANK = 'Redbank' const ROVER = 'Rover' @@ -49,17 +49,21 @@ export const main = async () => { : process.env.NETWORK === 'TESTNET' ? Network.TESTNET : Network.LOCALNET - const redbankConfig = getRedbankConfig(liquidatorMasterAddress, network, chainName) - const roverConfig = getRoverConfig(liquidatorMasterAddress, network, chainName) + const config = + executorType === REDBANK + ? getRedbankConfig(liquidatorMasterAddress, network, chainName) + : getRoverConfig(liquidatorMasterAddress, network, chainName) // Produce clients - const queryClient = await produceReadOnlyCosmWasmClient(process.env.RPC_ENDPOINT!) + const chainQuery = new ChainQuery( + process.env.LCD_ENDPOINT!, + process.env.APIKEY!, + config.contracts, + ) const client = await produceSigningStargateClient(process.env.RPC_ENDPOINT!, liquidator) const exchangeInterface = - chainName === 'osmosis' - ? new Osmosis() - : new AstroportCW(prefix, redbankConfig.astroportRouter!) + chainName === 'osmosis' ? new Osmosis() : new AstroportCW(prefix, config.astroportRouter!) const routeRequester = chainName === 'neutron' ? new AstroportRouteRequester(process.env.ASTROPORT_API_URL!) @@ -67,10 +71,22 @@ export const main = async () => { switch (executorType) { case REDBANK: - await launchRedbank(client, queryClient, redbankConfig, exchangeInterface, routeRequester) + await launchRedbank( + client, + chainQuery, + config as RedbankExecutorConfig, + exchangeInterface, + routeRequester, + ) return case ROVER: - await launchRover(client, queryClient, roverConfig, liquidator, routeRequester) + await launchRover( + client, + chainQuery, + config as RoverExecutorConfig, + liquidator, + routeRequester, + ) return default: throw new Error( @@ -80,29 +96,29 @@ export const main = async () => { } const launchRover = async ( - client: SigningStargateClient, - wasmClient: CosmWasmClient, + signingClient: SigningStargateClient, + queryClient: ChainQuery, roverConfig: RoverExecutorConfig, liquidatorWallet: DirectSecp256k1HdWallet, routeRequester: RouteRequester, ) => { - await new RoverExecutor(roverConfig, client, wasmClient, liquidatorWallet, routeRequester).start() + await new RoverExecutor( + roverConfig, + signingClient, + queryClient, + liquidatorWallet, + routeRequester, + ).start() } const launchRedbank = async ( client: SigningStargateClient, - wasmClient: CosmWasmClient, + query: ChainQuery, redbankConfig: RedbankExecutorConfig, exchangeInterface: Exchange, routeRequester: RouteRequester, ) => { - await new RedbankExecutor( - redbankConfig, - client, - wasmClient, - exchangeInterface, - routeRequester, - ).start() + await new RedbankExecutor(redbankConfig, client, query, exchangeInterface, routeRequester).start() } main().catch((e) => { diff --git a/src/query/chainQuery.ts b/src/query/chainQuery.ts new file mode 100644 index 0000000..698b3bf --- /dev/null +++ b/src/query/chainQuery.ts @@ -0,0 +1,139 @@ +import { Positions } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' +import { Market } from 'marsjs-types/mars-red-bank/MarsRedBank.types' +import { PriceResponse } from 'marsjs-types/mars-oracle-wasm/MarsOracleWasm.types' +import { Dictionary } from 'lodash' +import { TokensResponse } from 'marsjs-types/mars-account-nft/MarsAccountNft.types' +import { PriceSourceResponse } from '../types/oracle' +import { AssetParamsBaseForAddr, PerpParams } from 'marsjs-types/mars-params/MarsParams.types' +import { Balances, Collateral, Debt } from './types' +import fetch from 'cross-fetch' + +// Handle all contract queries here, so they are easily mockable. +export class ChainQuery { + constructor( + private lcdUrl: string, + private apiKey: string, + private contracts: Dictionary, + ) {} + + async queryContractSmart(msg: Object, contractAddress: string): Promise { + const base64Msg = Buffer.from(JSON.stringify(msg)).toString('base64') + const url = `${this.lcdUrl}/cosmwasm/wasm/v1/contract/${contractAddress}/smart/${base64Msg}?x-apikey=${this.apiKey}` + const response = await fetch(url) + return (await response.json())['data'] as T + } + + public async queryMarket(denom: string): Promise { + const msg = { + market: { + denom: denom, + }, + } + + return this.queryContractSmart(msg, this.contracts.redbank) + } + + public async queryMarkets(startAfter?: string, limit?: Number): Promise { + const msg = { + markets: { + start_after: startAfter, + limit: limit, + }, + } + + return this.queryContractSmart(msg, this.contracts.redbank) + } + + public async queryOraclePrice(denom: string): Promise { + const msg = { + price: { + denom: denom, + }, + } + + return this.queryContractSmart(msg, this.contracts.oracle) + } + + public async queryOraclePrices(startAfter?: String, limit?: Number): Promise { + const msg = { + prices: { + start_after: startAfter, + limit: limit, + }, + } + + return this.queryContractSmart(msg, this.contracts.oracle) + } + + public async queryOraclePriceSources( + startAfter?: String, + limit?: Number, + ): Promise { + const msg = { + price_sources: { + start_after: startAfter, + limit: limit, + }, + } + + return this.queryContractSmart(msg, this.contracts.oracle) + } + + public async queryPerpParams(startAfter?: String, limit?: Number): Promise { + const msg = { + all_perp_params: { + start_after: startAfter, + limit: limit, + }, + } + + return this.queryContractSmart(msg, this.contracts.params) + } + + public async queryPositionsForAccount(accountId: String): Promise { + const msg = { + positions: { + account_id: accountId, + }, + } + + return this.queryContractSmart(msg, this.contracts.creditManager) + } + + public async queryAllAssetParams( + startAfter?: String, + limit?: Number, + ): Promise { + let msg = { + all_asset_params: { + start_after: startAfter, + limit: limit, + }, + } + + return this.queryContractSmart(msg, this.contracts.params) + } + + public async queryAccountsForAddress(liquidatorAddress: String): Promise { + const msg = { + tokens: { owner: liquidatorAddress }, + } + return this.queryContractSmart(msg, this.contracts.accountNft) + } + + public async queryRedbankCollaterals(address: string): Promise { + const msg = { user_collaterals: { user: address } } + return this.queryContractSmart(msg, this.contracts.redbank) + } + + public async queryRedbankDebts(address: string): Promise { + const msg = { user_debts: { user: address } } + return this.queryContractSmart(msg, this.contracts.redbank) + } + + public async queryBalance(address: string): Promise { + const url = `${this.lcdUrl}/cosmos/bank/v1beta1/balances/${address}?x-apikey=${this.apiKey}` + const response = await fetch(url) + return (await response.json()) as Balances + } +} diff --git a/src/query/contractQuery.ts b/src/query/contractQuery.ts deleted file mode 100644 index 621d275..0000000 --- a/src/query/contractQuery.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Positions } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' -import { Market } from 'marsjs-types/mars-red-bank/MarsRedBank.types' -import { PriceResponse } from 'marsjs-types/mars-oracle-wasm/MarsOracleWasm.types' -import { Dictionary } from 'lodash' - -interface ContractQueryInterface { - queryOraclePrice: (denom: string) => Promise - queryOraclePrices: (startAfter?: string, limit?: number) => Promise - queryMarket: (denom: string) => Promise - queryMarkets: (startAfter?: string, limit?: number) => Promise - queryPositionsForAccount: (accountId: string) => Promise - queryContractSmart: (msg: Object, contractAddress: string) => Promise -} - -// Handle all contract queries here, so they are easily mockable. -export class ContractQuery implements ContractQueryInterface { - constructor( - private lcdUrl: string, - private apiKey: string, - private contracts: Dictionary, - ) {} - - async queryContractSmart(msg: Object, contractAddress: string): Promise { - const base64Msg = Buffer.from(JSON.stringify(msg)).toString('base64') - const url = `${this.lcdUrl}/cosmwasm/wasm/v1/contract/${contractAddress}/smart/${base64Msg}?x-apikey=${this.apiKey}` - const response = await fetch(url) - return (await response.json())['data'] as T - } - - public async queryMarket(denom: string): Promise { - const msg = { - market: { - denom: denom, - }, - } - - return this.queryContractSmart(msg, this.contracts.redbank) - } - - public async queryMarkets(startAfter?: string, limit?: Number): Promise { - const msg = { - markets: { - start_after: startAfter, - limit: limit, - }, - } - - return this.queryContractSmart(msg, this.contracts.redbank) - } - - public async queryOraclePrice(denom: string): Promise { - const msg = { - price: { - denom: denom, - }, - } - - return this.queryContractSmart(msg, this.contracts.oracle) - } - - public async queryOraclePrices(startAfter?: String, limit?: Number): Promise { - const msg = { - prices: { - start_after: startAfter, - limit: limit, - }, - } - - return this.queryContractSmart(msg, this.contracts.oracle) - } - - public async queryPositionsForAccount(accountId: String): Promise { - const msg = { - positions: { - account_id: accountId, - }, - } - - return this.queryContractSmart(msg, this.contracts.creditManager) - } -} diff --git a/src/query/hive.ts b/src/query/hive.ts deleted file mode 100644 index 9b4d41e..0000000 --- a/src/query/hive.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Position } from '../types/position' -import fetch from 'cross-fetch' -import { Coin, Positions } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' -import { MarketInfo } from '../rover/types/MarketInfo' -import { NO_ROVER_DATA } from '../rover/constants/errors' -import BigNumber from 'bignumber.js' -import { - produceCoreRoverDataQuery, - produceRoverAccountPositionQuery, - produceVaultQuery, -} from './queries/rover' -import { REDEEM_BASE } from '../constants' -import { produceRedbankGeneralQuery, produceUserPositionQuery } from './queries/redbank' -import { - CoreDataResponse, - DataResponse, - RoverData, - VaultDataResponse, - VaultInfo, - VaultInfoWasm, -} from './types' -import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' - -const produceVaultInfo = (vaultResponseData: VaultDataResponse): VaultInfo => { - // get the keys - note that our keys will either be the vault address (for the bank module) or 'wasm' for the wasm module - const vaultAddress = Object.keys(vaultResponseData)[0] - - const wasm: VaultInfoWasm = vaultResponseData[vaultAddress] as VaultInfoWasm - const totalSupply = wasm.totalSupply - const baseToken = wasm.info.base_token - const vaultToken = wasm.info.vault_token - const lpShareToVaultShareRatio = new BigNumber(wasm.redeem).dividedBy(REDEEM_BASE) - - return { vaultAddress, baseToken, totalSupply, vaultToken, lpShareToVaultShareRatio } -} - -export const fetchRoverData = async ( - hiveEndpoint: string, - address: string, - redbankAddress: string, - swapperAddress: string, - vaultAddresses: string[], - params_address: string, -): Promise => { - const coreQuery = produceCoreRoverDataQuery( - address, - redbankAddress, - swapperAddress, - params_address, - ) - - const queries = vaultAddresses.map((vault) => { - return { - query: produceVaultQuery(vault, REDEEM_BASE), - } - }) - - queries.push({ query: coreQuery }) - - const response = await fetch(hiveEndpoint, { - method: 'post', - body: JSON.stringify(queries), - headers: { 'Content-Type': 'application/json' }, - }) - - const result: { data: CoreDataResponse | VaultDataResponse }[] = await response.json() - - if (result.length === 0) { - throw new Error(NO_ROVER_DATA) - } - const coreData: CoreDataResponse = result.pop()!.data as CoreDataResponse - - const vaultMap = new Map() - - result.forEach((vaultResponse) => { - const vaultInfo: VaultInfo = produceVaultInfo(vaultResponse.data as VaultDataResponse) - vaultMap.set(vaultInfo.vaultAddress, vaultInfo) - }) - - return { - markets: coreData.wasm.markets, - masterBalance: coreData.bank.balance, - routes: coreData.wasm.routes, - vaultInfo: vaultMap, - whitelistedAssets: [], - } -} - -export const fetchRedbankData = async ( - hiveEndpoint: string, - address: string, - redbankAddress: string, -): Promise<{ - bank: { - balance: Coin[] - } - wasm: { - markets: MarketInfo[] - whitelistedAssets?: string[] - } -}> => { - const query = produceRedbankGeneralQuery(address, redbankAddress) - const response = await fetch(hiveEndpoint, { - method: 'post', - body: JSON.stringify({ query }), - headers: { 'Content-Type': 'application/json' }, - }) - const myData = await response.json() - return myData.data -} - -export const fetchRoverPosition = async ( - accountId: string, - creditManagerAddress: string, - hiveEndpoint: string, -): Promise => { - const query = { query: produceRoverAccountPositionQuery(accountId, creditManagerAddress) } - - // post to hive endpoint - const response = await fetch(hiveEndpoint, { - method: 'post', - body: JSON.stringify(query), - headers: { 'Content-Type': 'application/json' }, - }) - - const result = (await response.json()) as { - data: { - wasm: { - position: Positions - } - } - } - - return result.data.wasm.position -} - -export const fetchRedbankBatch = async ( - positions: Position[], - redbankAddress: string, - hiveEndpoint: string, -): Promise => { - const queries = positions.map((position) => { - return { - query: produceUserPositionQuery(position.Identifier, redbankAddress), - } - }) - - const response = await fetch(hiveEndpoint, { - method: 'post', - body: JSON.stringify(queries), - headers: { 'Content-Type': 'application/json' }, - }) - - return (await response.json()) as DataResponse[] -} - -export const fetchBalances = async ( - client: CosmWasmClient, - addresses: string[], - gasDenom: string, -): Promise> => { - const promises = addresses.map(async (address) => { - return { address, coin: await client.getBalance(address, gasDenom) } - }) - - const balances = await Promise.all(promises) - const balancesMap: Map = new Map() - - balances.forEach((balance) => { - // @ts-ignore - balancesMap.set(balance.address, [balance.coin]) - }) - - return balancesMap -} diff --git a/src/query/routing/OsmosisRouteRequester.ts b/src/query/routing/OsmosisRouteRequester.ts index 70b77dc..ef746c5 100644 --- a/src/query/routing/OsmosisRouteRequester.ts +++ b/src/query/routing/OsmosisRouteRequester.ts @@ -1,10 +1,12 @@ import { RequestRouteResponse, RouteRequester } from './RouteRequesterInterface' export class OsmosisRouteRequester extends RouteRequester { - // @ts-ignore todo before deploying update on osmosis requestRoute( + //@ts-ignore tokenInDenom: string, + //@ts-ignore tokenOutDenom: string, + //@ts-ignore tokenInAmount: string, ): Promise { throw new Error('Method not implemented.') diff --git a/src/query/types.ts b/src/query/types.ts index 6473b19..5c053de 100644 --- a/src/query/types.ts +++ b/src/query/types.ts @@ -1,10 +1,5 @@ -import BigNumber from 'bignumber.js' import { Coin } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' -import { MarketInfo } from '../rover/types/MarketInfo' -import { SwapperRoute } from '../types/swapper' -import { PriceResponse } from 'marsjs-types/mars-oracle-osmosis/MarsOracleOsmosis.types' - export interface AssetResponse extends Coin { denom: string amount_scaled: string @@ -19,61 +14,11 @@ export interface Collateral extends AssetResponse { } export interface UserPositionData { - [key: string]: { - debts: Debt[] - collaterals: Collateral[] - } -} - -export interface DataResponse { - data: UserPositionData -} - -export interface VaultInfo { - vaultAddress: string - baseToken: string - vaultToken: string - totalSupply: string - - // This is how much lp token there is per share in a vault - lpShareToVaultShareRatio: BigNumber -} - -export interface RoverData { - masterBalance: Coin[] - markets: MarketInfo[] - whitelistedAssets: string[] - routes: SwapperRoute[] - vaultInfo: Map -} - -export interface CoreDataResponse { - bank: Balances - wasm: { - markets: MarketInfo[] - prices: PriceResponse[] - whitelistedAssets: string[] - routes: SwapperRoute[] - } + address: string + debts: Debt[] + collaterals: Collateral[] } export interface Balances { - balance: Coin[] -} - -export interface VaultInfoWasm { - totalSupply: string - info: { - base_token: string - vault_token: string - } - redeem: string -} - -export interface VaultDataResponse { - [key: string]: VaultInfoWasm -} - -export interface LiquidatorBalanceResponse { - [key: string]: Balances + balances: Coin[] } diff --git a/src/redbank/LiquidationHelpers.ts b/src/redbank/LiquidationHelpers.ts index 5b892d3..7752e86 100644 --- a/src/redbank/LiquidationHelpers.ts +++ b/src/redbank/LiquidationHelpers.ts @@ -1,7 +1,6 @@ import BigNumber from 'bignumber.js' import { Collateral, Debt } from '../query/types' import { AssetParamsBaseForAddr, Coin } from 'marsjs-types/mars-params/MarsParams.types' - export const calculatePositionLtv = ( debts: Debt[], collaterals: Collateral[], diff --git a/src/redbank/RedbankExecutor.ts b/src/redbank/RedbankExecutor.ts index 381ddcc..b0a78ad 100644 --- a/src/redbank/RedbankExecutor.ts +++ b/src/redbank/RedbankExecutor.ts @@ -8,13 +8,12 @@ import { EncodeObject } from '@cosmjs/proto-signing' import { produceExecuteContractMessage, produceWithdrawMessage, sleep } from '../helpers' import { cosmwasm } from 'osmojs' import 'dotenv/config.js' -import { fetchRedbankBatch } from '../query/hive' import BigNumber from 'bignumber.js' import { BaseExecutor, BaseConfig } from '../BaseExecutor' -import { CosmWasmClient, MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate' +import { MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate' import { getLargestCollateral, getLargestDebt } from '../liquidationGenerator' -import { Collateral, DataResponse } from '../query/types.js' +import { Collateral, UserPositionData } from '../query/types.js' import { Exchange } from '../exchange/ExchangeInterface.js' import { @@ -24,7 +23,7 @@ import { getLiquidationThresholdHealthFactor, } from './LiquidationHelpers' import { RouteRequester } from '../query/routing/RouteRequesterInterface' -import { AssetParamsBaseForAddr } from 'marsjs-types/mars-params/MarsParams.types' +import { ChainQuery } from '../query/chainQuery' const { executeContract } = cosmwasm.wasm.v1.MessageComposer.withTypeUrl @@ -46,13 +45,12 @@ export const XYX_PRICE_SOURCE = 'xyk_liquidity_token' export class RedbankExecutor extends BaseExecutor { private MINUTE = 1000 * 60 public config: RedbankExecutorConfig - private assetParams: Map = new Map() private targetHealthFactor: number = 0 constructor( config: RedbankExecutorConfig, client: SigningStargateClient, - queryClient: CosmWasmClient, + queryClient: ChainQuery, private exchangeInterface: Exchange, routeRequester: RouteRequester, ) { @@ -61,11 +59,11 @@ export class RedbankExecutor extends BaseExecutor { } async start() { - await this.fetchAssetParams() + await this.updateAssetParams() await this.fetchTargetHealthFactor() - await this.refreshData() + await this.init() - setInterval(this.fetchAssetParams, 10 * this.MINUTE) + setInterval(this.updateAssetParams, 10 * this.MINUTE) setInterval(this.updatePriceSources, 10 * this.MINUTE) setInterval(this.updateOraclePrices, 1 * this.MINUTE) setInterval(this.fetchTargetHealthFactor, 10 * this.MINUTE) @@ -83,56 +81,19 @@ export class RedbankExecutor extends BaseExecutor { async fetchTargetHealthFactor() { try { this.targetHealthFactor = await this.queryClient.queryContractSmart( - this.config.marsParamsAddress, { target_health_factor: {}, }, + this.config.contracts.params, ) } catch (e) { console.error(e) } } - async fetchAssetParams() { - const maxRetries = 5 - const limit = 5 - - // while not returning empty, get all asset params - let fetching = true - let startAfter = '' - let retries = 0 - while (fetching) { - try { - const response = await this.queryClient.queryContractSmart(this.config.marsParamsAddress, { - all_asset_params: { - limit, - start_after: startAfter, - }, - }) - - startAfter = response[response.length - 1] ? response[response.length - 1].denom : '' - response.forEach((assetParam: AssetParamsBaseForAddr) => { - this.assetParams.set(assetParam.denom, assetParam) - }) - fetching = response.length === 5 - retries = 0 - } catch (ex) { - console.warn(ex) - retries++ - if (retries > maxRetries) { - console.warn('Max retries exceeded, exiting', maxRetries) - fetching = false - } else { - await sleep(5000) - console.info('Retrying...') - } - } - } - } - - async produceLiquidationTxs(positionData: DataResponse[]): Promise<{ - txs: LiquidationTx[] - debtsToRepay: Map + async produceLiquidationTx(positionData: UserPositionData): Promise<{ + tx: LiquidationTx + debtToRepay: Coin }> { const txs: LiquidationTx[] = [] const debtsToRepay = new Map() @@ -145,125 +106,122 @@ export class RedbankExecutor extends BaseExecutor { throw new Error('No neutral asset available') } - // create a list of debts that need to be liquidated - for (const positionResponse of positionData) { - const positionAddress = Object.keys(positionResponse.data)[0] - const position = positionResponse.data[positionAddress] - - if (position.collaterals.length > 0 && position.debts.length > 0) { - // Build our params - const largestCollateral = getLargestCollateral(position.collaterals, this.prices) - const largestCollateralDenom = largestCollateral.denom - const largestDebt = getLargestDebt(position.debts, this.prices) - const debtDenom = largestDebt.denom - const debtParams = this.assetParams.get(debtDenom) - const collateralParams = this.assetParams.get(largestCollateralDenom) - const debtPrice = this.prices.get(debtDenom) - const collateralPrice = this.prices.get(largestCollateralDenom) - - if (!debtParams || !debtPrice || !collateralPrice || !collateralParams) continue - const lbStart = Number(collateralParams.liquidation_bonus.starting_lb) - const lbSlope = Number(collateralParams.liquidation_bonus.slope) - const lbMax = Number(collateralParams.liquidation_bonus.max_lb) - const lbMin = Number(collateralParams.liquidation_bonus.min_lb) - const protocolLiquidationFee = Number(debtParams.protocol_liquidation_fee) - - const ltHealthFactor = getLiquidationThresholdHealthFactor( - position.collaterals, - position.debts, - this.prices, - this.assetParams, - ) + if (positionData.collaterals.length > 0 && positionData.debts.length > 0) { + // Build our params + const largestCollateral = getLargestCollateral(positionData.collaterals, this.prices) + const largestCollateralDenom = largestCollateral.denom + const largestDebt = getLargestDebt(positionData.debts, this.prices) + const debtDenom = largestDebt.denom + const debtParams = this.assetParams.get(debtDenom)! + const collateralParams = this.assetParams.get(largestCollateralDenom)! + const debtPrice = this.prices.get(debtDenom)! + const collateralPrice = this.prices.get(largestCollateralDenom)! + + const lbStart = Number(collateralParams.liquidation_bonus.starting_lb) + const lbSlope = Number(collateralParams.liquidation_bonus.slope) + const lbMax = Number(collateralParams.liquidation_bonus.max_lb) + const lbMin = Number(collateralParams.liquidation_bonus.min_lb) + const protocolLiquidationFee = Number(debtParams.protocol_liquidation_fee) + + const ltHealthFactor = getLiquidationThresholdHealthFactor( + positionData.collaterals, + positionData.debts, + this.prices, + this.assetParams, + ) - const liquidationBonus = calculateLiquidationBonus( - lbStart, - lbSlope, - ltHealthFactor, - lbMax, - lbMin, - calculateCollateralRatio(position.debts, position.collaterals, this.prices).toNumber(), - ) - // Neutral available to us for this specific liquidation - const remainingNeutral = availableValue.minus(totalDebtValue) - - // max debt the protocol will allow us to repay - const maxDebtRepayableValue = calculateMaxDebtRepayable( - this.targetHealthFactor, - position.debts, - position.collaterals, - this.assetParams, - liquidationBonus, + const liquidationBonus = calculateLiquidationBonus( + lbStart, + lbSlope, + ltHealthFactor, + lbMax, + lbMin, + calculateCollateralRatio( + positionData.debts, + positionData.collaterals, this.prices, - largestCollateralDenom, - ) - // Cap the repay amount by the collateral we are claiming. We need to factor in the liquidation bonus - as that comes out of the available collateral - const largestCollateralValue = new BigNumber(largestCollateral.amount) - .multipliedBy(1 - liquidationBonus) - .multipliedBy(collateralPrice) - if ( - !largestCollateralValue || - largestCollateralValue.isLessThanOrEqualTo(10000) || - largestCollateralValue.isNaN() - ) - continue + ).toNumber(), + ) - // todo Make sure that the max repayable is less than the debt - const maxRepayableValue = maxDebtRepayableValue.isGreaterThan(largestCollateralValue) - ? largestCollateralValue - : maxDebtRepayableValue - const maxRepayableAmount = maxRepayableValue.dividedBy(this.prices.get(debtDenom) || 0) - - // Cap the repay amount by the remaining neutral asset we have - const amountToRepay = remainingNeutral.isGreaterThan(maxRepayableValue) - ? maxRepayableAmount - : remainingNeutral.multipliedBy(0.95).dividedBy(debtPrice) - - // If our debt is the same as our neutral, we skip this step - const buyDebtRoute = - this.config.neutralAssetDenom === debtDenom - ? [] - : this.ammRouter.getBestRouteGivenOutput( - this.config.neutralAssetDenom, - debtDenom, - amountToRepay, - ) - - console.log({ - amountToRepay: JSON.stringify(amountToRepay), - buyDebtRoute: JSON.stringify(buyDebtRoute), - maxDebtRepayableValue: JSON.stringify(maxDebtRepayableValue), - maxRepayableAmount: JSON.stringify(maxRepayableAmount), - maxRepayableValue: JSON.stringify(maxRepayableValue), - remainingNeutral: JSON.stringify(remainingNeutral), - neutralAssetDenom: this.config.neutralAssetDenom, - debtDenom: debtDenom, - debtPrice: debtPrice, - collateralPrice: collateralPrice, - liquidationBonus: liquidationBonus, - protocolLiquidationFee: protocolLiquidationFee, - }) - - const liquidateTx = { - collateral_denom: largestCollateralDenom, - debt_denom: largestDebt.denom, - user_address: positionAddress, - amount: amountToRepay.multipliedBy(0.98).toFixed(0), - } + // max debt the protocol will allow us to repay + const maxDebtRepayableValue = calculateMaxDebtRepayable( + this.targetHealthFactor, + positionData.debts, + positionData.collaterals, + this.assetParams, + liquidationBonus, + this.prices, + largestCollateralDenom, + ) + // Cap the repay amount by the collateral we are claiming. We need to factor in the liquidation bonus - as that comes out of the available collateral + const largestCollateralValue = new BigNumber(largestCollateral.amount) + .multipliedBy(1 - liquidationBonus) + .multipliedBy(collateralPrice) + + // todo Make sure that the max repayable is less than the debt + const maxRepayableValue = maxDebtRepayableValue.isGreaterThan(largestCollateralValue) + ? largestCollateralValue + : maxDebtRepayableValue + const maxRepayableAmount = maxRepayableValue.dividedBy(this.prices.get(debtDenom) || 0) + + // Neutral available to us for this specific liquidation + const remainingNeutral = availableValue.minus(totalDebtValue) + + // Cap the repay amount by the remaining neutral asset we have + const amountToRepay = remainingNeutral.isGreaterThan(maxRepayableValue) + ? maxRepayableAmount + : remainingNeutral.multipliedBy(0.95).dividedBy(debtPrice) + + // If our debt is the same as our neutral, we skip this step + const buyDebtRoute = + this.config.neutralAssetDenom === debtDenom + ? [] + : this.ammRouter.getBestRouteGivenOutput( + this.config.neutralAssetDenom, + debtDenom, + amountToRepay, + ) + + console.log({ + amountToRepay: JSON.stringify(amountToRepay), + buyDebtRoute: JSON.stringify(buyDebtRoute), + maxDebtRepayableValue: JSON.stringify(maxDebtRepayableValue), + maxRepayableAmount: JSON.stringify(maxRepayableAmount), + maxRepayableValue: JSON.stringify(maxRepayableValue), + remainingNeutral: JSON.stringify(remainingNeutral), + neutralAssetDenom: this.config.neutralAssetDenom, + debtDenom: debtDenom, + debtPrice: debtPrice, + collateralPrice: collateralPrice, + liquidationBonus: liquidationBonus, + protocolLiquidationFee: protocolLiquidationFee, + }) - const newTotalDebt = totalDebtValue.plus( - new BigNumber(amountToRepay).multipliedBy(debtPrice), - ) + const liquidateTx = { + collateral_denom: largestCollateralDenom, + debt_denom: largestDebt.denom, + user_address: positionData.address, + amount: amountToRepay.multipliedBy(0.98).toFixed(0), + } - txs.push(liquidateTx) + const newTotalDebt = totalDebtValue.plus(new BigNumber(amountToRepay).multipliedBy(debtPrice)) - // update debts + totals - const existingDebt = debtsToRepay.get(liquidateTx.debt_denom) || 0 - debtsToRepay.set(liquidateTx.debt_denom, new BigNumber(amountToRepay).plus(existingDebt)) - totalDebtValue = newTotalDebt + txs.push(liquidateTx) + + // update debts + totals + const existingDebt = debtsToRepay.get(liquidateTx.debt_denom) || 0 + debtsToRepay.set(liquidateTx.debt_denom, new BigNumber(amountToRepay).plus(existingDebt)) + totalDebtValue = newTotalDebt + return { + tx: liquidateTx, + debtToRepay: { + denom: liquidateTx.debt_denom, + amount: amountToRepay.toFixed(0), + }, } } - return { txs, debtsToRepay } + throw new Error('Missing collateral or debt for position') } appendWithdrawMessages( @@ -276,7 +234,7 @@ export class RedbankExecutor extends BaseExecutor { const denom = collateral.denom msgs.push( executeContract( - produceWithdrawMessage(liquidatorAddress, denom, this.config.redbankAddress) + produceWithdrawMessage(liquidatorAddress, denom, this.config.contracts.redbank) .value as MsgExecuteContract, ), ) @@ -389,7 +347,7 @@ export class RedbankExecutor extends BaseExecutor { return produceExecuteContractMessage( this.config.liquidatorMasterAddress, - this.config.redbankAddress, + this.config.contracts.redbank, toUtf8(msg), [ { @@ -403,16 +361,14 @@ export class RedbankExecutor extends BaseExecutor { async run(): Promise { const liquidatorAddress = this.config.liquidatorMasterAddress - if (!this.queryClient || !this.client) + if (!this.queryClient || !this.signingClient) throw new Error("Instantiate your clients before calling 'run()'") - await this.refreshData() + await this.init() - const collateralsBefore: Collateral[] = await this.queryClient?.queryContractSmart( - this.config.redbankAddress, - { user_collaterals: { user: liquidatorAddress } }, + const collateralsBefore: Collateral[] = await this.queryClient.queryRedbankCollaterals( + liquidatorAddress, ) - await this.liquidateCollaterals(liquidatorAddress, collateralsBefore) const url = `${this.config @@ -447,87 +403,70 @@ export class RedbankExecutor extends BaseExecutor { return } - for (const position of positions) { - console.log(`- Liquidating ${position.Identifier}`) - // Fetch position data - const positionData: DataResponse[] = await fetchRedbankBatch( - [position], - this.config.redbankAddress, - this.config.hiveEndpoint, - ) + const positionToLiquidate = positions[0] - const { txs, debtsToRepay } = await this.produceLiquidationTxs(positionData) - const debtCoins: Coin[] = [] - debtsToRepay.forEach((amount, denom) => debtCoins.push({ denom, amount: amount.toFixed(0) })) - // deposit any neutral in our account before starting liquidations - const firstMsgBatch: EncodeObject[] = [] - await this.appendSwapToDebtMessages( - debtCoins, - liquidatorAddress, - firstMsgBatch, - new BigNumber(this.balances.get(this.config.neutralAssetDenom)!), - ) + console.log(`- Liquidating ${positionToLiquidate.Identifier}`) + // Fetch position data + const liquidateeDebts = await this.queryClient.queryRedbankDebts(positionToLiquidate.Identifier) + const liquidateeCollaterals = await this.queryClient.queryRedbankCollaterals( + positionToLiquidate.Identifier, + ) - // Preferably, we liquidate via redbank directly. This is so that if the liquidation fails, - // the entire transaction fails and we do not swap. - // When using the liquidation filterer contract, transactions with no successfull liquidations - // will still succeed, meaning that we will still swap to the debt and have to swap back again. - // If liquidating via redbank, unsucessfull liquidations will error, preventing the swap - const execute: MsgExecuteContractEncodeObject = this.executeViaRedbankMsg(txs[0]) - firstMsgBatch.push(execute) + const { tx, debtToRepay } = await this.produceLiquidationTx({ + address: positionToLiquidate.Identifier, + debts: liquidateeDebts, + collaterals: liquidateeCollaterals, + }) + // deposit any neutral in our account before starting liquidations + const firstMsgBatch: EncodeObject[] = [] + await this.appendSwapToDebtMessages( + [debtToRepay], + liquidatorAddress, + firstMsgBatch, + new BigNumber(this.balances.get(this.config.neutralAssetDenom)!), + ) - if (!firstMsgBatch || firstMsgBatch.length === 0 || txs.length === 0) continue + // Preferably, we liquidate via redbank directly. This is so that if the liquidation fails, + // the entire transaction fails and we do not swap. + // When using the liquidation filterer contract, transactions with no successfull liquidations + // will still succeed, meaning that we will still swap to the debt and have to swap back again. + // If liquidating via redbank, unsucessfull liquidations will error, preventing the swap + const execute: MsgExecuteContractEncodeObject = this.executeViaRedbankMsg(tx) + firstMsgBatch.push(execute) - const firstFee = await this.getFee( - firstMsgBatch, - this.config.liquidatorMasterAddress, - this.config.chainName.toLowerCase(), - ) + const firstFee = await this.getFee( + firstMsgBatch, + this.config.liquidatorMasterAddress, + this.config.chainName.toLowerCase(), + ) - const result = await this.client.signAndBroadcast( - this.config.liquidatorMasterAddress, - firstMsgBatch, - firstFee, - ) + const result = await this.signingClient.signAndBroadcast( + this.config.liquidatorMasterAddress, + firstMsgBatch, + firstFee, + ) - console.log(`Liquidation hash: ${result.transactionHash}`) + console.log(`Liquidation hash: ${result.transactionHash}`) - console.log(`- Successfully liquidated ${txs.length} positions`) + const collaterals: Collateral[] = await this.queryClient.queryRedbankCollaterals( + liquidatorAddress, + ) - const collaterals: Collateral[] = await this.queryClient?.queryContractSmart( - this.config.redbankAddress, - { user_collaterals: { user: liquidatorAddress } }, - ) + await this.liquidateCollaterals(liquidatorAddress, collaterals) - await this.liquidateCollaterals(liquidatorAddress, collaterals) - - await this.setBalances(liquidatorAddress) - - if (this.config.logResults) { - txs.forEach((tx) => { - this.addCsvRow({ - blockHeight: result.height, - collateral: tx.collateral_denom, - debtRepaid: tx.debt_denom, - estimatedLtv: '0', - userAddress: tx.user_address, - liquidatorBalance: Number(this.balances.get(this.config.neutralAssetDenom) || 0), - }) - }) - } + await this.setBalances(liquidatorAddress) - console.log(`- Liquidation Process Complete.`) + console.log(`- Liquidation Process Complete.`) - if (this.config.logResults) { - this.writeCsv() - } + if (this.config.logResults) { + this.writeCsv() } } async liquidateCollaterals(liquidatorAddress: string, collaterals: Collateral[]) { let msgs: EncodeObject[] = [] - const balances = await this.client?.getAllBalances(liquidatorAddress) + const balances = await this.signingClient?.getAllBalances(liquidatorAddress) const combinedCoins = this.combineBalances(collaterals, balances!).filter( (coin) => @@ -546,7 +485,7 @@ export class RedbankExecutor extends BaseExecutor { this.config.liquidatorMasterAddress, this.config.chainName.toLowerCase(), ) - await this.client.signAndBroadcast(this.config.liquidatorMasterAddress, msgs, secondFee) + await this.signingClient.signAndBroadcast(this.config.liquidatorMasterAddress, msgs, secondFee) } combineBalances(collaterals: Collateral[], balances: readonly Coin[]): Coin[] { diff --git a/src/redbank/config/neutron.ts b/src/redbank/config/neutron.ts index 2053216..e413a4d 100644 --- a/src/redbank/config/neutron.ts +++ b/src/redbank/config/neutron.ts @@ -1,3 +1,4 @@ +import { mapValues } from 'lodash' import { Network } from '../../types/network' import { RedbankExecutorConfig } from '../RedbankExecutor' @@ -10,34 +11,58 @@ export const getConfig = ( chainName: 'neutron', productName: 'redbank', safetyMargin: 0.05, + contracts: mapValues({ + addressProvider: 'neutron17yehp4x7n79zq9dlw4g7xmnrvwdjjj2yecq26844sg8yu74knlxqfx5vqv', + redbank: 'neutron1n97wnm7q6d2hrcna3rqlnyqw2we6k0l8uqvmyqq6gsml92epdu7quugyph', + incentives: 'neutron1aszpdh35zsaz0yj80mz7f5dtl9zq5jfl8hgm094y0j0vsychfekqxhzd39', + oracle: 'neutron1dwp6m7pdrz6rnhdyrx5ha0acsduydqcpzkylvfgspsz60pj2agxqaqrr7g', + rewardsCollector: 'neutron1h4l6rvylzcuxwdw3gzkkdzfjdxf4mv2ypfdgvnvag0dtz6x07gps6fl2vm', + swapper: 'neutron1udr9fc3kd743dezrj38v2ac74pxxr6qsx4xt4nfpcfczgw52rvyqyjp5au', + params: 'neutron1x4rgd7ry23v2n49y7xdzje0743c5tgrnqrqsvwyya2h6m48tz4jqqex06x', + zapper: 'neutron1dr0ckm3u2ztjuscmgqjr85lwyduphxkgl3tc02ac8zp54r05t5dqp5tgyq', + health: 'neutron17ktfwsr7ghlxzzma0gw0hke3j3rnssd58q87jv2wzfrk6uhawa3sv8xxtm', + creditManager: 'neutron1qdzn3l4kn7gsjna2tfpg3g3mwd6kunx4p50lfya59k02846xas6qslgs3r', + accountNft: 'neutron184kvu96rqtetmunkkmhu5hru8yaqg7qfhd8ldu5avjnamdqu69squrh3f5', + }), astroportRouter: 'neutron1rwj6mfxzzrwskur73v326xwuff52vygqk73lr7azkehnfzz5f5wskwekf4', lcdEndpoint: process.env.LCD_ENDPOINT!, // use env vars in order to be able to quickly change gasDenom: 'untrn', - hiveEndpoint: process.env.HIVE_ENDPOINT!, liquidatorMasterAddress: liquidatorMasterAddress, logResults: false, - marsParamsAddress: 'neutron1x4rgd7ry23v2n49y7xdzje0743c5tgrnqrqsvwyya2h6m48tz4jqqex06x', neutralAssetDenom: 'ibc/B559A80D62249C8AA07A380E2A2BEA6E5CA9A6F079C912C3A9E9B494105E4F81', - oracleAddress: 'neutron1dwp6m7pdrz6rnhdyrx5ha0acsduydqcpzkylvfgspsz60pj2agxqaqrr7g', - redbankAddress: 'neutron1n97wnm7q6d2hrcna3rqlnyqw2we6k0l8uqvmyqq6gsml92epdu7quugyph', liquidationProfitMarginPercent: 0.001, poolsRefreshWindow: 60000, - marsEndpoint: 'https://api.marsprotocol.io', + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://api.marsprotocol.io', astroportFactory: 'neutron1hptk0k5kng7hjy35vmh009qd5m6l33609nypgf2yc6nqnewduqasxplt4e', } : { safetyMargin: 0.05, chainName: 'neutron', productName: 'redbank', + contracts: mapValues({ + addressProvider: 'neutron1qr8wfk59ep3fmhyulhg75dw68dxrq7v6qfzufglgs4ry5wptx47sytnkav', + redbank: 'neutron19ucpt6vyha2k6tgnex880sladcqsguwynst4f8krh9vuxhktwkvq3yc3nl', + incentives: 'neutron1xqfgy03gulfyv6dnz9ezsjkgcvsvlaajskw35cluux9g05cmcu4sfdkuvc', + oracle: 'neutron12vejgch3jd74j99kdrpjf57f6zjlu425yyfscdjnmnn4vvyrazvqgvcp24', + rewardsCollector: 'neutron1dnh5urdl2e4ylpfzxgfd82lf5l3ydy5gync4tar35ax9c6lrv0fsgkqx9n', + swapper: 'neutron1dyltrt8aekyprrs3l838r02cpceed48hjtz3x8vqrzm0tukm3ktqtp5j49', + params: 'neutron14a0qr0ahrg3f3yml06m9f0xmvw30ldf3scgashcjw5mrtyrc4aaq0v4tm9', + zapper: 'neutron13kvhvvem9t78shv8k9jrc6rsvjjnwhvylg3eh3qgssd4dx2234kq5aaekn', + health: 'neutron14v200h6tawndkct9nenrg4x5kh0888kd8lx6l95m4932z2n5zn0qdfhtcq', + creditManager: 'neutron1zkxezh5e6jvg0h3kj50hz5d0yrgagkp0c3gcdr6stulw7fye9xlqygj2gz', + accountNft: 'neutron1pgk4ttz3ned9xvqlg79f4jumjet0443uqh2rga9ahalzgxqngtrqrszdna', + perps: 'neutron1dcv8sy6mhgjaum5tj8lghxgxx2jgf3gmcw6kg73rj70sx5sjpguslzv0xu', + }), lcdEndpoint: process.env.LCD_ENDPOINT!, // use env vars in order to be able to quickly change gasDenom: 'untrn', - hiveEndpoint: process.env.HIVE_ENDPOINT!, liquidatorMasterAddress: liquidatorMasterAddress, logResults: false, // enable for debugging + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://testnet-api.marsprotocol.io', neutralAssetDenom: 'ibc/EFB00E728F98F0C4BBE8CA362123ACAB466EDA2826DC6837E49F4C1902F21BBA', - oracleAddress: 'neutron1nx9txtmpmkt58gxka20z72wdkguw4n0606zkeqvelv7q7uc06zmsym3qgx', - marsParamsAddress: 'todo', - redbankAddress: 'neutron15dld0kmz0zl89zt4yeks4gy8mhmawy3gp4x5rwkcgkj5krqvu9qs4q7wve', astroportRouter: 'neutron12jm24l9lr9cupufqjuxpdjnnweana4h66tsx5cl800mke26td26sq7m05p', astroportFactory: 'neutron1jj0scx400pswhpjes589aujlqagxgcztw04srynmhf0f6zplzn2qqmhwj7', liquidationProfitMarginPercent: 0.001, diff --git a/src/redbank/config/osmosis.ts b/src/redbank/config/osmosis.ts index 505d329..ad5ade0 100644 --- a/src/redbank/config/osmosis.ts +++ b/src/redbank/config/osmosis.ts @@ -1,3 +1,4 @@ +import { mapValues } from 'lodash' import { Network } from '../../types/network' import { RedbankExecutorConfig } from '../RedbankExecutor' @@ -10,36 +11,57 @@ export const getConfig = ( safetyMargin: 0.05, chainName: 'osmosis', productName: 'redbank', + contracts: mapValues({ + addressProvider: 'osmo1g677w7mfvn78eeudzwylxzlyz69fsgumqrscj6tekhdvs8fye3asufmvxr', + redbank: 'osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg', + incentives: 'osmo1nkahswfr8shg8rlxqwup0vgahp0dk4x8w6tkv3rra8rratnut36sk22vrm', + oracle: 'osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g', + rewardsCollector: 'osmo1urvqe5mw00ws25yqdd4c4hlh8kdyf567mpcml7cdve9w08z0ydcqvsrgdy', + swapper: 'osmo1wee0z8c7tcawyl647eapqs4a88q8jpa7ddy6nn2nrs7t47p2zhxswetwla', + zapper: 'osmo17qwvc70pzc9mudr8t02t3pl74hhqsgwnskl734p4hug3s8mkerdqzduf7c', + creditManager: 'osmo1f2m24wktq0sw3c0lexlg7fv4kngwyttvzws3a3r3al9ld2s2pvds87jqvf', + accountNft: 'osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09', + params: 'osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent', + health: 'osmo1pdc49qlyhpkzx4j24uuw97kk6hv7e9xvrdjlww8qj6al53gmu49sge4g79', + }), lcdEndpoint: process.env.LCD_ENDPOINT!, // use env vars in order to be able to quickly change gasDenom: 'uosmo', - hiveEndpoint: process.env.HIVE_ENDPOINT!, liquidatorMasterAddress: liquidatorMasterAddress, logResults: false, // enable for debugging neutralAssetDenom: 'ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4', - oracleAddress: 'osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g', - marsParamsAddress: - process.env.MARS_PARAMS_ADDRESS || - 'osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent', - redbankAddress: 'osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg', poolsRefreshWindow: 60000, liquidationProfitMarginPercent: 0.01, - marsEndpoint: 'https://api.marsprotocol.io', + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://api.marsprotocol.io', sqsUrl: process.env.SQS_URL!, } : { chainName: 'osmosis', productName: 'redbank', + contracts: mapValues({ + addressProvider: 'osmo1g677w7mfvn78eeudzwylxzlyz69fsgumqrscj6tekhdvs8fye3asufmvxr', + redbank: 'osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg', + incentives: 'osmo1nkahswfr8shg8rlxqwup0vgahp0dk4x8w6tkv3rra8rratnut36sk22vrm', + oracle: 'osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g', + rewardsCollector: 'osmo1urvqe5mw00ws25yqdd4c4hlh8kdyf567mpcml7cdve9w08z0ydcqvsrgdy', + swapper: 'osmo1wee0z8c7tcawyl647eapqs4a88q8jpa7ddy6nn2nrs7t47p2zhxswetwla', + zapper: 'osmo17qwvc70pzc9mudr8t02t3pl74hhqsgwnskl734p4hug3s8mkerdqzduf7c', + creditManager: 'osmo1f2m24wktq0sw3c0lexlg7fv4kngwyttvzws3a3r3al9ld2s2pvds87jqvf', + accountNft: 'osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09', + params: 'osmo1aye5qcer5n52crrkaf35jprsad2807q6kg3eeeu7k79h4slxfausfqhc9y', + health: 'osmo1kqzkuyh23chjwemve7p9t7sl63v0sxtjh84e95w4fdz3htg8gmgspua7q4', + }), safetyMargin: 0.05, lcdEndpoint: process.env.LCD_ENDPOINT!, gasDenom: 'uosmo', - hiveEndpoint: process.env.HIVE_ENDPOINT!, liquidatorMasterAddress: liquidatorMasterAddress, logResults: false, // enable for debugging neutralAssetDenom: 'uosmo', // no usdc pools on testnet - oracleAddress: 'osmo1dqz2u3c8rs5e7w5fnchsr2mpzzsxew69wtdy0aq4jsd76w7upmsstqe0s8', - marsParamsAddress: '', - redbankAddress: 'osmo1t0dl6r27phqetfu0geaxrng0u9zn8qgrdwztapt5xr32adtwptaq6vwg36', poolsRefreshWindow: 60000, + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://testnet-api.marsprotocol.io', liquidationProfitMarginPercent: 0.01, sqsUrl: process.env.SQS_URL!, } diff --git a/src/rover/ActionGenerator.ts b/src/rover/ActionGenerator.ts index 6641256..35eaf6d 100644 --- a/src/rover/ActionGenerator.ts +++ b/src/rover/ActionGenerator.ts @@ -9,11 +9,14 @@ import { DebtAmount, } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' import BigNumber from 'bignumber.js' -import { POOL_NOT_FOUND, UNSUPPORTED_VAULT } from './constants/errors' import { calculateTotalPerpPnl, queryAstroportLpUnderlyingTokens } from '../helpers' -import { VaultInfo } from '../query/types' -import { PoolType, XYKPool } from '../types/Pool' import { RouteRequester } from '../query/routing/RouteRequesterInterface' +import { + LiquidationAmountInputs, + calculate_liquidation_amounts_js, + HealthData, +} from 'mars-liquidation-node' +import { AssetParamsBaseForAddr } from 'marsjs-types/mars-params/MarsParams.types.js' export class ActionGenerator { private routeRequester: RouteRequester @@ -26,6 +29,8 @@ export class ActionGenerator { account: Positions, oraclePrices: Map, redbankMarkets: Map, + assetParams: Map, + healthData: HealthData, neutralDenom: string, ): Promise => { // Find highest value collateral. Note that is merely the largest collateral by oracle price. @@ -54,9 +59,34 @@ export class ActionGenerator { denom: neutralDenom, value: new BigNumber(0), } - : this.findBestDebt(account.debts, oraclePrices) + : this.findLargestDebt(account.debts, oraclePrices) + + const liquidationAmountInputs: LiquidationAmountInputs = { + collateral_amount: collateral.amount.toFixed(0), + collateral_price: oraclePrices.get(collateral.denom)!.toFixed(18), + collateral_params: assetParams.get(collateral.denom)!, + debt_amount: debt.amount.toFixed(0), + debt_params: assetParams.get(debt.denom)!, + debt_requested_to_repay: debt.amount.toFixed(0), + debt_price: oraclePrices.get(debt.denom)!.toFixed(18), + health: healthData, + // TODO: do we need to query this? + perps_lb_ratio: new BigNumber(0.1).toString(), + } + + const liqHf: number = liquidationAmountInputs.health.liquidation_health_factor + if (liqHf == null || liqHf >= 1) { + console.log(`Position with id ${account.account_id} is not liquidatable. HF : ${liqHf}`) + return [] + } - let borrowActions: Action[] = this.produceBorrowActions(debt, collateral, redbankMarkets) + const liquidationAmounts = calculate_liquidation_amounts_js(liquidationAmountInputs) + let borrowActions: Action[] = this.produceBorrowActions( + debt.denom, + liquidationAmounts.debt_amount, + collateral, + redbankMarkets, + ) // variables const { borrow } = borrowActions[0] as { borrow: Coin } @@ -65,7 +95,7 @@ export class ActionGenerator { const liquidateAction = this.produceLiquidationAction( collateral.type, - { denom: debt.denom, amount: debt.amount.toFixed(0) }, + { denom: debt.denom, amount: liquidationAmounts.debt_amount.toString() }, account.account_id, collateral.denom, ) @@ -73,7 +103,10 @@ export class ActionGenerator { const collateralToDebtActions = collateral.denom !== borrow.denom ? await this.swapCollateralCoinToDebtActions( - collateral.denom, + { + amount: liquidationAmounts.collateral_amount_received_by_liquidator.toString(), + denom: collateral.denom, + }, borrow, slippage, oraclePrices, @@ -128,7 +161,8 @@ export class ActionGenerator { * @param collateral The largest collateral in the position */ produceBorrowActions = ( - debt: Debt, + debtDenom: string, + maxRepayableAmount: BigNumber, collateral: Collateral, //@ts-ignore - to be used for todos in method markets: map, @@ -148,9 +182,10 @@ export class ActionGenerator { // // debt amount is a number, not a value (e.g in dollar / base asset denominated terms) // let debtAmount = debtToRepayRatio.multipliedBy(debt.amount) + console.log(maxRepayableAmount) const debtCoin: Coin = { - amount: debt.amount.toFixed(0), - denom: debt.denom, + amount: maxRepayableAmount.toString(), + denom: debtDenom, } // TODO - check if asset is whitelisted @@ -212,62 +247,6 @@ export class ActionGenerator { } } - produceVaultToDebtActions = async ( - vault: VaultInfo, - borrow: Coin, - slippage: string, - prices: Map, - ): Promise => { - let vaultActions: Action[] = [] - if (!vault) throw new Error(UNSUPPORTED_VAULT) - - const lpTokenDenom = vault.baseToken - const poolId = lpTokenDenom.split('/').pop() - - // withdraw lp - const withdraw = this.produceWithdrawLiquidityAction(lpTokenDenom) - - vaultActions.push(withdraw) - - //@ts-ignore - // Convert pool assets to borrowed asset - const pool = this.router.getPool(poolId!) - - // todo log id - this shouldn't happen though - if (!pool) throw new Error(`${POOL_NOT_FOUND} : ${poolId}`) - - // todo = support CL/Stableswap on rover - if ( - pool.poolType === PoolType.CONCENTRATED_LIQUIDITY || - pool.poolType === PoolType.STABLESWAP - ) { - return [] - } - - let filteredPools = (pool as XYKPool).poolAssets.filter( - (poolAsset) => poolAsset.token.denom !== borrow.denom, - ) - - for (const poolAsset of filteredPools) { - const assetOutPrice = prices.get(borrow.denom)! - const assetInPrice = prices.get(poolAsset.token.denom)! - const amountIn = new BigNumber(assetOutPrice.dividedBy(assetInPrice)).multipliedBy( - borrow.amount, - ) - vaultActions.push( - await this.generateSwapActions( - poolAsset.token.denom, - borrow.denom, - assetInPrice, - assetOutPrice, - amountIn.toFixed(0), - slippage, - ), - ) - } - return vaultActions - } - produceWithdrawLiquidityAction = (lpTokenDenom: string): Action => { return { withdraw_liquidity: { @@ -304,16 +283,14 @@ export class ActionGenerator { * @returns An array of swap actions that convert the collateral to the debt. */ swapCollateralCoinToDebtActions = async ( - collateralDenom: string, + collateralWon: Coin, borrowed: Coin, slippage: string, prices: Map, ): Promise => { let actions: Action[] = [] - - console.log(JSON.stringify(prices.keys())) - console.log(collateralDenom) - console.log(borrowed.denom) + const collateralDenom = collateralWon.denom + const collateralAmount = new BigNumber(collateralWon.amount) const assetInPrice = prices.get(collateralDenom)! const assetOutPrice = prices.get(borrowed.denom)! @@ -325,14 +302,9 @@ export class ActionGenerator { const underlyingDenoms = (await queryAstroportLpUnderlyingTokens(collateralDenom))! for (const denom of underlyingDenoms) { if (denom !== borrowed.denom) { - // TODO This is a very rough approximation. We could optimise and make more accurate - const amountIn = assetOutPrice - .dividedBy(assetInPrice) - .multipliedBy(borrowed.amount) - .dividedBy(underlyingDenoms.length) - // This could be a source of bugs, if the amount of underlying tokens in the pools - // are not even. So we err on the side of caution. - .multipliedBy(0.5) + // TODO: This could be a source of bugs, if the amount of underlying tokens in the pools + // are not even. + const amountIn = collateralAmount.multipliedBy(0.5) actions = actions.concat( await this.generateSwapActions( denom, @@ -346,19 +318,13 @@ export class ActionGenerator { } } } else { - // TODO this is a rough approximation - let amountIn = assetOutPrice - .dividedBy(assetInPrice) - .multipliedBy(borrowed.amount) - .multipliedBy(0.8) - actions = actions.concat( await this.generateSwapActions( collateralDenom, borrowed.denom, assetInPrice, assetOutPrice, - amountIn.toFixed(0), + collateralAmount.toFixed(0), slippage, ), ) @@ -496,7 +462,7 @@ export class ActionGenerator { return amountBn.multipliedBy(oraclePrice).toNumber() } - findBestDebt = (debts: DebtAmount[], oraclePrices: Map): Debt => { + findLargestDebt = (debts: DebtAmount[], oraclePrices: Map): Debt => { if (debts.length === 0) throw new Error('Error: No debts found') return debts .map((debtAmount) => { diff --git a/src/rover/RoverExecutor.ts b/src/rover/RoverExecutor.ts index bbac6b5..1de6f7e 100644 --- a/src/rover/RoverExecutor.ts +++ b/src/rover/RoverExecutor.ts @@ -6,24 +6,30 @@ import { sleep, } from '../helpers' import { toUtf8 } from '@cosmjs/encoding' -import { fetchBalances } from '../query/hive' + import { ActionGenerator } from './ActionGenerator' -import { Coin, Positions } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' +import { + Addr, + Coin, + Positions, + VaultPositionValue, +} from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' import BigNumber from 'bignumber.js' import { MsgSendEncodeObject, SigningStargateClient } from '@cosmjs/stargate' -import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { DirectSecp256k1HdWallet, EncodeObject } from '@cosmjs/proto-signing' -import { QueryMsg, VaultConfigBaseForString } from 'marsjs-types/mars-params/MarsParams.types' +import { VaultConfigBaseForString } from 'marsjs-types/mars-params/MarsParams.types' import { RouteRequester } from '../query/routing/RouteRequesterInterface' +import { compute_health_js, HealthComputer } from 'mars-rover-health-computer-node' +import { TokensResponse } from 'marsjs-types/mars-account-nft/MarsAccountNft.types' +import { ChainQuery } from '../query/chainQuery' +import { HealthData } from 'mars-liquidation-node' + interface CreateCreditAccountResponse { tokenId: number liquidatorAddress: string } export interface RoverExecutorConfig extends BaseConfig { - creditManagerAddress: string - swapperAddress: string - accountNftAddress: string minGasTokens: number maxLiquidators: number stableBalanceThreshold: number @@ -44,7 +50,7 @@ export class RoverExecutor extends BaseExecutor { constructor( config: RoverExecutorConfig, client: SigningStargateClient, - queryClient: CosmWasmClient, + queryClient: ChainQuery, wallet: DirectSecp256k1HdWallet, routeRequester: RouteRequester, ) { @@ -56,7 +62,7 @@ export class RoverExecutor extends BaseExecutor { // Entry to rover executor start = async () => { - await this.refreshData() + await this.init() // set up accounts const accounts = await this.wallet.getAccounts() @@ -71,7 +77,7 @@ export class RoverExecutor extends BaseExecutor { const createCreditAccountpromises: Promise[] = [] liquidatorAddresses.map((address) => - createCreditAccountpromises.push(this.createCreditAccount(address)), + createCreditAccountpromises.push(this.getDefaultCreditAccount(address)), ) const results: CreateCreditAccountResponse[] = await Promise.all(createCreditAccountpromises) @@ -82,85 +88,137 @@ export class RoverExecutor extends BaseExecutor { // We set up 3 separate tasks to run in parallel // // Periodically fetch the different pieces of data we need, - setInterval(this.refreshData, 30 * 1000) + setInterval(this.init, 30 * 1000) // Ensure our liquidator wallets have more than enough funds to operate setInterval(this.updateLiquidatorBalances, 20 * 1000) // check for and dispatch liquidations - setInterval(this.run, 1000) + while (true) { + await this.run() + await sleep(5000) + } } run = async () => { - // Pop latest unhealthy positions from the list - cap this by the number of liquidators we have available - const url = `${this.config.marsEndpoint!}/v2/unhealthy_positions?chain=${ - this.config.chainName - }&product=${this.config.productName}` - - const response = await fetch(url) - let targetAccountObjects: { - account_id: string - health_factor: string - total_debt: string - }[] = (await response.json())['data'] - - const targetAccounts = targetAccountObjects - .filter( - (account) => - Number(account.health_factor) < Number(process.env.MAX_LIQUIDATION_LTV) && - Number(account.health_factor) > Number(process.env.MIN_LIQUIDATION_LTV), - // To target specific accounts, filter here - ) - .sort((accountA, accountB) => Number(accountB.total_debt) - Number(accountA.total_debt)) - // Sleep to avoid spamming. + try { + // Pop latest unhealthy positions from the list - cap this by the number of liquidators we have available + const url = `${this.config.marsEndpoint!}/v2/unhealthy_positions?chain=${ + this.config.chainName + }&product=${this.config.productName}` + + const response = await fetch(url) + let targetAccountObjects: { + account_id: string + health_factor: string + total_debt: string + }[] = (await response.json())['data'] + + const targetAccounts = targetAccountObjects + .filter( + (account) => + Number(account.health_factor) < Number(process.env.MAX_LIQUIDATION_LTV) && + Number(account.health_factor) > Number(process.env.MIN_LIQUIDATION_LTV), + // To target specific accounts, filter here + ) + .sort((accountA, accountB) => Number(accountB.total_debt) - Number(accountA.total_debt)) + // Sleep to avoid spamming. - if (targetAccounts.length == 0) { - await sleep(2000) - return - } + if (targetAccounts.length == 0) { + await sleep(2000) + return + } - // create chunks of accounts to liquidate - const unhealthyAccountChunks = [] - for (let i = 0; i < targetAccounts.length; i += this.liquidatorAccounts.size) { - unhealthyAccountChunks.push(targetAccounts.slice(i, i + this.liquidatorAccounts.size)) - } - // iterate over chunks and liquidate - for (const chunk of unhealthyAccountChunks) { - const liquidatorAddressesIterator = this.liquidatorAccounts.keys() - const liquidationPromises: Promise[] = [] - for (const account of chunk) { - const nextLiquidator = liquidatorAddressesIterator.next() - console.log('liquidating: ', account.account_id, ' with ', nextLiquidator.value) - liquidationPromises.push(this.liquidate(account.account_id, nextLiquidator.value!)) + // create chunks of accounts to liquidate + const unhealthyAccountChunks = [] + for (let i = 0; i < targetAccounts.length; i += this.liquidatorAccounts.size) { + unhealthyAccountChunks.push(targetAccounts.slice(i, i + this.liquidatorAccounts.size)) + } + // iterate over chunks and liquidate + for (const chunk of unhealthyAccountChunks) { + const liquidatorAddressesIterator = this.liquidatorAccounts.keys() + const liquidationPromises: Promise[] = [] + for (const account of chunk) { + const nextLiquidator = liquidatorAddressesIterator.next() + console.log('liquidating: ', account.account_id, ' with ', nextLiquidator.value) + liquidationPromises.push(this.liquidate(account.account_id, nextLiquidator.value!)) + } + await Promise.all(liquidationPromises) + await sleep(4000) + } + } catch (ex) { + if (process.env.DEBUG) { + console.error(ex) } - await Promise.all(liquidationPromises) - await sleep(4000) } } liquidate = async (accountId: string, liquidatorAddress: string) => { try { - const account: Positions = await this.queryClient.queryContractSmart( - this.config.creditManagerAddress, - { positions: { account_id: accountId } }, - ) - + const account: Positions = await this.queryClient.queryPositionsForAccount(accountId) const updatedAccount: Positions = calculatePositionStateAfterPerpClosure( account, this.config.neutralAssetDenom, ) - const actions = this.liquidationActionGenerator.generateLiquidationActions( + // Make prices safe for our wasm. If we just to string we + // get things like 3.42558449e-9 which cannot be parsed + // by the health computer + let checkedPrices: Map = new Map() + this.prices.forEach((price, denom) => { + checkedPrices.set(denom, price.toFixed(18)) + }) + + let hc: HealthComputer = { + kind: updatedAccount.account_kind, + positions: updatedAccount, + asset_params: Object.fromEntries(this.assetParams), + vaults_data: { + vault_values: new Map(), + vault_configs: new Map(), + }, + perps_data: { + params: Object.fromEntries(this.perpParams), + }, + oracle_prices: Object.fromEntries(checkedPrices), + } + + const healthResponse = compute_health_js(hc) + + const accountNetValue = new BigNumber(healthResponse.total_collateral_value) + .minus(healthResponse.total_debt_value) + .toFixed(0) + const collateralizationRatio = + healthResponse.total_debt_value === '0' + ? new BigNumber(100000000) // Instead of `infinity` we use a very high number + : new BigNumber(healthResponse.total_collateral_value) + .dividedBy(new BigNumber(healthResponse.total_debt_value)) + .toFixed(0) + + const healthData: HealthData = { + liquidation_health_factor: healthResponse.liquidation_health_factor, + account_net_value: accountNetValue, + collateralization_ratio: collateralizationRatio, + perps_pnl_loss: healthResponse.perps_pnl_loss, + } + + const actions = await this.liquidationActionGenerator.generateLiquidationActions( updatedAccount, this.prices, this.markets, + this.assetParams, + healthData, this.config.neutralAssetDenom, ) - const liquidatorAccountId = this.liquidatorAccounts.get(liquidatorAddress) + if (actions.length === 0) { + return + } + + const liquidatorAccountId = this.liquidatorAccounts.get(liquidatorAddress)! // Produce message const msg = { update_credit_account: { - account_id: liquidatorAccountId, + account_id: liquidatorAccountId.toString(), actions, }, } @@ -168,7 +226,7 @@ export class RoverExecutor extends BaseExecutor { const msgs: EncodeObject[] = [ produceExecuteContractMessage( liquidatorAddress, - this.config.creditManagerAddress, + this.config.contracts.creditManager, toUtf8(JSON.stringify(msg)), ), ] @@ -190,13 +248,9 @@ export class RoverExecutor extends BaseExecutor { msgs.push(sendMsg) } - const fee = await this.getFee( - msgs, - this.config.liquidatorMasterAddress, - this.config.chainName.toLowerCase(), - ) + const fee = await this.getFee(msgs, liquidatorAddress, this.config.chainName.toLowerCase()) - const result = await this.client.signAndBroadcast(liquidatorAddress, msgs, fee) + const result = await this.signingClient.signAndBroadcast(liquidatorAddress, msgs, fee) if (result.code !== 0) { console.log(`Liquidation failed. TxHash: ${result.transactionHash}`) @@ -221,17 +275,22 @@ export class RoverExecutor extends BaseExecutor { ensureWorkerMinBalance = async (addresses: string[]) => { try { - const balances = await fetchBalances(this.queryClient, addresses, this.config.gasDenom) - this.liquidatorBalances = balances const sendMsgs: MsgSendEncodeObject[] = [] const amountToSend = this.config.minGasTokens * 2 - for (const address of Array.from(balances.keys())) { - const osmoBalance = Number( - balances.get(address)?.find((coin: Coin) => coin.denom === this.config.gasDenom) - ?.amount || 0, + + for (const address of addresses) { + let balancesResponse = await this.queryClient.queryBalance(address) + // Update our balances with latest amounts + this.liquidatorBalances.set(address, balancesResponse.balances) + + // Check gas balance, send more if required + let gasDenomBalance = balancesResponse.balances.find( + (coin) => coin.denom === this.config.gasDenom, ) - if (osmoBalance === undefined || osmoBalance < this.config.minGasTokens) { - // send message to send gas tokens to our liquidator + if ( + gasDenomBalance === undefined || + new BigNumber(gasDenomBalance.amount).isLessThan(this.config.minGasTokens) + ) { sendMsgs.push( produceSendMessage(this.config.liquidatorMasterAddress, address, [ { denom: this.config.gasDenom, amount: amountToSend.toFixed(0) }, @@ -246,7 +305,11 @@ export class RoverExecutor extends BaseExecutor { this.config.liquidatorMasterAddress, this.config.chainName.toLowerCase(), ) - await this.client.signAndBroadcast(this.config.liquidatorMasterAddress, sendMsgs, fee) + await this.signingClient.signAndBroadcast( + this.config.liquidatorMasterAddress, + sendMsgs, + fee, + ) } } catch (ex) { console.error(ex) @@ -254,37 +317,7 @@ export class RoverExecutor extends BaseExecutor { } } - fetchVaults = async () => { - let foundAll = false - const limit = 5 - let vaults: VaultConfigBaseForString[] = [] - let startAfter: string | undefined = undefined - while (!foundAll) { - const vaultQuery: QueryMsg = { - all_vault_configs: { - limit, - start_after: startAfter, - }, - } - - const results: VaultConfigBaseForString[] = await this.queryClient.queryContractSmart( - this.config.marsParamsAddress, - vaultQuery, - ) - - vaults = vaults.concat(results) - - if (results.length < limit) { - foundAll = true - } - - startAfter = results.pop()?.addr - } - - return vaults - } - - refreshData = async () => { + init = async () => { try { // Periodically refresh the vaults we have const currentTimeMs = Date.now() @@ -303,30 +336,33 @@ export class RoverExecutor extends BaseExecutor { // this.config.marsParamsAddress, // ) - await this.refreshMarketData() + await this.updateMarketsData() await this.updatePriceSources() await this.updateOraclePrices() - // roverData.masterBalance.forEach((coin) => this.balances.set(coin.denom, Number(coin.amount))) - // this.vaultInfo = roverData.vaultInfo + await this.updatePerpParams() + await this.updateAssetParams() } catch (ex) { console.error('Failed to refresh data') console.error(JSON.stringify(ex)) } } - createCreditAccount = async (liquidatorAddress: string): Promise => { - let { tokens } = await this.queryClient.queryContractSmart(this.config.accountNftAddress, { - tokens: { owner: liquidatorAddress }, - }) + getDefaultCreditAccount = async ( + liquidatorAddress: string, + ): Promise => { + let tokensResponse: TokensResponse = await this.queryClient.queryAccountsForAddress( + liquidatorAddress, + ) + let msgs = [ produceExecuteContractMessage( liquidatorAddress, - this.config.creditManagerAddress, + this.config.contracts.creditManager, toUtf8(`{ "create_credit_account": "default" }`), ), ] - if (tokens.length === 0) { - const result = await this.client.signAndBroadcast( + if (tokensResponse.tokens.length === 0) { + const result = await this.signingClient.signAndBroadcast( liquidatorAddress, msgs, await this.getFee(msgs, liquidatorAddress, this.config.chainName.toLowerCase()), @@ -339,16 +375,11 @@ export class RoverExecutor extends BaseExecutor { } // todo parse result to get sub account id - const { tokens: updatedTokens } = await this.queryClient.queryContractSmart( - this.config.accountNftAddress, - { - tokens: { owner: liquidatorAddress }, - }, - ) + const newTokensResponse = await this.queryClient.queryAccountsForAddress(liquidatorAddress) - tokens = updatedTokens + tokensResponse = newTokensResponse } - return { liquidatorAddress, tokenId: tokens[0] } + return { liquidatorAddress, tokenId: Number(tokensResponse.tokens[0]) } } } diff --git a/src/rover/config/neutron.ts b/src/rover/config/neutron.ts index 07d4c76..2e8e577 100644 --- a/src/rover/config/neutron.ts +++ b/src/rover/config/neutron.ts @@ -1,3 +1,4 @@ +import { mapValues } from 'lodash' import { Network } from '../../types/network' import { RoverExecutorConfig as CreditManagerConfig } from '../RoverExecutor' @@ -11,28 +12,22 @@ export const getConfig = ( chainName: 'neutron', // Rover is called creditmanager in the api productName: 'creditmanager', - hiveEndpoint: process.env.HIVE_ENDPOINT!, + contracts: mapValues({ + addressProvider: 'neutron1fg5v00sa0x3avsxa4rft5v9sgktl3s6fvkjwxy03lplcc6hrqxps08u2lc', + redbank: 'neutron1xucw5lg7sh9gmupd90jaeupvq0nm4pj5esu3ff7f64pacy2lyjsqfwft80', + incentives: 'neutron1uf6nclgqvwnqv5lfverunenpzyw556h739sekj75k62h062k9lrqzhm3up', + oracle: 'neutron14rjfsglulewu9narj077ata6p0dkfjjuayguku50f8tg2fyf4ups44a0ww', + rewardsCollector: 'neutron1l0ehl3wptumpyg85csv6n5dky93h4sph4ypfjpztnu4cj7kg9uvstzlwrr', + swapper: 'neutron1t29va54hgzsakwuh2azpr77ty793h57yd978gz0dkekvyqrpcupqhhy6g3', + params: 'neutron102xprj349yslxu5xncpsmv8qk38ryag870xvgxgm5r9dnagvetwszssu59', + zapper: 'neutron16604kpsj3uptdxharvdn5w4ps3j7lydudn0dprwnmg5aj35uhatqse2l37', + health: 'neutron18g6w7vkqwkdkexzl227g5h7464lzx4et4l5w9aawp8j7njf6gjkqrzpuug', + creditManager: 'neutron1eekxmplmetd0eq2fs6lyn5lrds5nwa92gv5nw6ahjjlu8xudm2xs03784t', + accountNft: 'neutron1jdpceeuzrptvrvvln3f72haxwl0w38peg6ux76wrm3d265ghne7se4wug2', + }), lcdEndpoint: process.env.LCD_ENDPOINT!, neutralAssetDenom: 'ibc/B559A80D62249C8AA07A380E2A2BEA6E5CA9A6F079C912C3A9E9B494105E4F81', //neutralAssetDenom: 'ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858', - swapperAddress: - process.env.SWAPPER_ADDRESS || - 'neutron1udr9fc3kd743dezrj38v2ac74pxxr6qsx4xt4nfpcfczgw52rvyqyjp5au', - oracleAddress: - process.env.ORACLE_ADDRESS || - 'neutron1dwp6m7pdrz6rnhdyrx5ha0acsduydqcpzkylvfgspsz60pj2agxqaqrr7g', - redbankAddress: - process.env.REDBANK_ADDRESS || - 'neutron1n97wnm7q6d2hrcna3rqlnyqw2we6k0l8uqvmyqq6gsml92epdu7quugyph', - accountNftAddress: - process.env.ACCOUNT_NFT_ADDRESS || - 'neutron184kvu96rqtetmunkkmhu5hru8yaqg7qfhd8ldu5avjnamdqu69squrh3f5', - marsParamsAddress: - process.env.MARS_PARAMS_ADDRESS || - 'neutron1x4rgd7ry23v2n49y7xdzje0743c5tgrnqrqsvwyya2h6m48tz4jqqex06x', - creditManagerAddress: - process.env.CREDIT_MANAGER_ADDRESS || - 'neutron1qdzn3l4kn7gsjna2tfpg3g3mwd6kunx4p50lfya59k02846xas6qslgs3r', liquidatorMasterAddress: liquidatorMasterAddress, minGasTokens: 1000000, logResults: false, @@ -40,7 +35,9 @@ export const getConfig = ( maxLiquidators: process.env.MAX_LIQUIDATORS ? parseInt(process.env.MAX_LIQUIDATORS) : 10, stableBalanceThreshold: 5000000, // marsEndpoint: "http://127.0.0.1:3000", - marsEndpoint: 'https://api.marsprotocol.io', + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://api.marsprotocol.io', sqsUrl: 'https://sqs.osmosis.zone/', } } @@ -50,34 +47,31 @@ export const getConfig = ( gasDenom: 'untrn', chainName: 'neutron', productName: 'creditmanager', - hiveEndpoint: process.env.HIVE_ENDPOINT!, lcdEndpoint: process.env.LCD_ENDPOINT!, neutralAssetDenom: 'factory/neutron1ke0vqqzyymlp5esr8gjwuzh94ysnpvj8er5hm7/UUSDC', // no usdc pools on testnet so we use osmo - swapperAddress: - process.env.SWAPPER_ADDRESS || - 'neutron12xuseg6l3q6g6e928chmvzqus92m9tw6ajns88yg9ww5crx58djshwlqya', - oracleAddress: - process.env.ORACLE_ADDRESS || - 'neutron1pev35y62g6vte0s9t67gsf6m8d60x36t7wr0p0ghjl9r3h5mwl0q4h2zwc', - redbankAddress: - process.env.REDBANK_ADDRESS || - 'neutron1f8ag222s4rnytkweym7lfncrxhtee3za5uk54r5n2rjxvsl9slzq36f66d', - accountNftAddress: - process.env.ACCOUNT_NFT_ADDRESS || - 'neutron1hx27cs7jjuvwq4hqgxn4av8agnspy2nwvrrq8e9f80jkeyrwrh8s8x645z', - marsParamsAddress: - process.env.MARS_PARAMS_ADDRESS || - 'neutron1q66e3jv2j9r0duzwzt37fwl7h5njhr2kqs0fxmaa58sfqke80a2ss5hrz7', - creditManagerAddress: - process.env.CREDIT_MANAGER_ADDRESS || - 'neutron13vyqc4efsnc357ze97ppv9h954zjasuj9d0w8es3mk9ea8sg6mvsr3xkjg', + contracts: mapValues({ + addressProvider: 'neutron1qr8wfk59ep3fmhyulhg75dw68dxrq7v6qfzufglgs4ry5wptx47sytnkav', + redbank: 'neutron19ucpt6vyha2k6tgnex880sladcqsguwynst4f8krh9vuxhktwkvq3yc3nl', + incentives: 'neutron1xqfgy03gulfyv6dnz9ezsjkgcvsvlaajskw35cluux9g05cmcu4sfdkuvc', + oracle: 'neutron12vejgch3jd74j99kdrpjf57f6zjlu425yyfscdjnmnn4vvyrazvqgvcp24', + rewardsCollector: 'neutron1dnh5urdl2e4ylpfzxgfd82lf5l3ydy5gync4tar35ax9c6lrv0fsgkqx9n', + swapper: 'neutron1dyltrt8aekyprrs3l838r02cpceed48hjtz3x8vqrzm0tukm3ktqtp5j49', + params: 'neutron14a0qr0ahrg3f3yml06m9f0xmvw30ldf3scgashcjw5mrtyrc4aaq0v4tm9', + zapper: 'neutron13kvhvvem9t78shv8k9jrc6rsvjjnwhvylg3eh3qgssd4dx2234kq5aaekn', + health: 'neutron14v200h6tawndkct9nenrg4x5kh0888kd8lx6l95m4932z2n5zn0qdfhtcq', + creditManager: 'neutron1zkxezh5e6jvg0h3kj50hz5d0yrgagkp0c3gcdr6stulw7fye9xlqygj2gz', + accountNft: 'neutron1pgk4ttz3ned9xvqlg79f4jumjet0443uqh2rga9ahalzgxqngtrqrszdna', + perps: 'neutron1dcv8sy6mhgjaum5tj8lghxgxx2jgf3gmcw6kg73rj70sx5sjpguslzv0xu', + }), liquidatorMasterAddress: liquidatorMasterAddress, minGasTokens: 1000000, logResults: false, poolsRefreshWindow: 60000, maxLiquidators: process.env.MAX_LIQUIDATORS ? parseInt(process.env.MAX_LIQUIDATORS) : 1, stableBalanceThreshold: 5000000, - marsEndpoint: 'https://testnet-api.marsprotocol.io', + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://testnet-api.marsprotocol.io', sqsUrl: 'https://sqs.osmosis.zone/', } } diff --git a/src/rover/config/osmosis.ts b/src/rover/config/osmosis.ts index 943da70..7ea028f 100644 --- a/src/rover/config/osmosis.ts +++ b/src/rover/config/osmosis.ts @@ -1,3 +1,4 @@ +import { mapValues } from 'lodash' import { Network } from '../../types/network' import { RoverExecutorConfig } from '../RoverExecutor' @@ -10,36 +11,31 @@ export const getConfig = ( gasDenom: 'uosmo', chainName: 'osmosis', productName: 'creditmanager', - hiveEndpoint: process.env.HIVE_ENDPOINT!, + contracts: mapValues({ + addressProvider: 'osmo1g677w7mfvn78eeudzwylxzlyz69fsgumqrscj6tekhdvs8fye3asufmvxr', + redbank: 'osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg', + incentives: 'osmo1nkahswfr8shg8rlxqwup0vgahp0dk4x8w6tkv3rra8rratnut36sk22vrm', + oracle: 'osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g', + rewardsCollector: 'osmo1urvqe5mw00ws25yqdd4c4hlh8kdyf567mpcml7cdve9w08z0ydcqvsrgdy', + swapper: 'osmo1wee0z8c7tcawyl647eapqs4a88q8jpa7ddy6nn2nrs7t47p2zhxswetwla', + zapper: 'osmo17qwvc70pzc9mudr8t02t3pl74hhqsgwnskl734p4hug3s8mkerdqzduf7c', + creditManager: 'osmo1f2m24wktq0sw3c0lexlg7fv4kngwyttvzws3a3r3al9ld2s2pvds87jqvf', + accountNft: 'osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09', + params: 'osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent', + health: 'osmo1pdc49qlyhpkzx4j24uuw97kk6hv7e9xvrdjlww8qj6al53gmu49sge4g79', + }), lcdEndpoint: process.env.LCD_ENDPOINT!, neutralAssetDenom: 'ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4', //neutralAssetDenom: 'ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858', - swapperAddress: - process.env.SWAPPER_ADDRESS || - 'osmo1wee0z8c7tcawyl647eapqs4a88q8jpa7ddy6nn2nrs7t47p2zhxswetwla', - oracleAddress: - process.env.ORACLE_ADDRESS || - 'osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g', - redbankAddress: - process.env.REDBANK_ADDRESS || - 'osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg', - accountNftAddress: - process.env.ACCOUNT_NFT_ADDRESS || - 'osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09', - marsParamsAddress: - process.env.MARS_PARAMS_ADDRESS || - 'osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent', - creditManagerAddress: - process.env.CREDIT_MANAGER_ADDRESS || - 'osmo1f2m24wktq0sw3c0lexlg7fv4kngwyttvzws3a3r3al9ld2s2pvds87jqvf', liquidatorMasterAddress: liquidatorMasterAddress, minGasTokens: 1000000, logResults: false, poolsRefreshWindow: 60000, maxLiquidators: process.env.MAX_LIQUIDATORS ? parseInt(process.env.MAX_LIQUIDATORS) : 10, stableBalanceThreshold: 5000000, - // marsEndpoint: "http://127.0.0.1:3000", - marsEndpoint: 'https://api.marsprotocol.io', + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://api.marsprotocol.io', sqsUrl: 'https://sqs.osmosis.zone/', } } @@ -49,32 +45,29 @@ export const getConfig = ( gasDenom: 'uosmo', chainName: 'osmosis', productName: 'creditmanager', - hiveEndpoint: process.env.HIVE_ENDPOINT!, + contracts: mapValues({ + addressProvider: 'osmo1g677w7mfvn78eeudzwylxzlyz69fsgumqrscj6tekhdvs8fye3asufmvxr', + redbank: 'osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg', + incentives: 'osmo1nkahswfr8shg8rlxqwup0vgahp0dk4x8w6tkv3rra8rratnut36sk22vrm', + oracle: 'osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g', + rewardsCollector: 'osmo1urvqe5mw00ws25yqdd4c4hlh8kdyf567mpcml7cdve9w08z0ydcqvsrgdy', + swapper: 'osmo1wee0z8c7tcawyl647eapqs4a88q8jpa7ddy6nn2nrs7t47p2zhxswetwla', + zapper: 'osmo17qwvc70pzc9mudr8t02t3pl74hhqsgwnskl734p4hug3s8mkerdqzduf7c', + creditManager: 'osmo1f2m24wktq0sw3c0lexlg7fv4kngwyttvzws3a3r3al9ld2s2pvds87jqvf', + accountNft: 'osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09', + params: 'osmo1aye5qcer5n52crrkaf35jprsad2807q6kg3eeeu7k79h4slxfausfqhc9y', + health: 'osmo1kqzkuyh23chjwemve7p9t7sl63v0sxtjh84e95w4fdz3htg8gmgspua7q4', + }), lcdEndpoint: process.env.LCD_ENDPOINT!, neutralAssetDenom: 'uosmo', // no usdc pools on testnet so we use osmo - swapperAddress: - process.env.SWAPPER_ADDRESS || - 'osmo17c4retwuyxjxzv9f2q9r0272s8smktpzhjetssttxxdavarjtujsjqafa2', - oracleAddress: - process.env.ORACLE_ADDRESS || - 'osmo1dh8f3rhg4eruc9w7c9d5e06eupqqrth7v32ladwkyphvnn66muzqxcfe60', - redbankAddress: - process.env.REDBANK_ADDRESS || - 'osmo1pvrlpmdv3ee6lgmxd37n29gtdahy4tec7c5nyer9aphvfr526z6sff9zdg', - accountNftAddress: - process.env.ACCOUNT_NFT_ADDRESS || - 'osmo1j0m37hqpaeh79cjrdna4sep6yfyu278rrm4qta6s4hjq6fv3njxqsvhcex', - marsParamsAddress: - process.env.MARS_PARAMS_ADDRESS || - 'osmo1dpwu03xc45vpqur6ry69xjhltq4v0snrhaukcp4fvhucx0wypzhs978lnp', - creditManagerAddress: - process.env.CREDIT_MANAGER_ADDRESS || - 'osmo12wd0rwuvu7wwujztkww5c7sg4fw4e6t235jyftwy5ydc48uxd24q4s9why', liquidatorMasterAddress: liquidatorMasterAddress, minGasTokens: 10000000, logResults: false, poolsRefreshWindow: 60000, maxLiquidators: 100, + marsEndpoint: process.env.MARS_API_ENDPOINT + ? process.env.MARS_API_ENDPOINT + : 'https://testnet-api.marsprotocol.io', stableBalanceThreshold: 5000000, sqsUrl: 'https://sqs.osmosis.zone/', } diff --git a/src/rover/types/MarketInfo.ts b/src/rover/types/MarketInfo.ts index 3ee2a54..6bcfe4d 100644 --- a/src/rover/types/MarketInfo.ts +++ b/src/rover/types/MarketInfo.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js' -import { MarketV2Response } from 'marsjs-types/mars-red-bank/MarsRedBank.types' +import { Market } from 'marsjs-types/mars-red-bank/MarsRedBank.types' -export interface MarketInfo extends MarketV2Response { +export interface MarketInfo extends Market { available_liquidity: BigNumber } diff --git a/src/secretManager.ts b/src/secretManager.ts index 48d1b13..8ec3fb4 100644 --- a/src/secretManager.ts +++ b/src/secretManager.ts @@ -1,17 +1,29 @@ -export interface SecretManager { - getSeedPhrase(): Promise -} +// for types - see original +import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager' +export const getSecretManager = () => { + const client = new SecretsManagerClient({ + region: 'ap-southeast-1', + }) -export const getSecretManager = (): SecretManager => { return { - getSeedPhrase: async () => { - const seed = process.env.SEED - if (!seed) - throw Error( - 'Failed to find SEED environment variable. Add your seed phrase to the SEED environment variable or implement a secret manager instance', + getSeedPhrase: async (): Promise => { + const mnemonic = process.env.WALLET_MNEMONIC + if (mnemonic) { + return mnemonic + } else { + const secretName = process.env.WALLET_MNEMONIC_SECRET_NAME + console.log('Fetching mnemonic') + const response = await client.send( + new GetSecretValueCommand({ + SecretId: secretName, + VersionStage: 'AWSCURRENT', // VersionStage defaults to AWSCURRENT if unspecified + }), ) - return seed + const secret = JSON.parse(response.SecretString!) + console.log('Successfully retrieved mnemonic') + return Object.values(secret)[0] as string + } }, } } diff --git a/test/liquidationGenerator.test.ts b/test/liquidationGenerator.test.ts index a2baa8b..575f532 100644 --- a/test/liquidationGenerator.test.ts +++ b/test/liquidationGenerator.test.ts @@ -14,7 +14,7 @@ describe('Liquidation Tx Generator Tests..', () => { const assets = [collateralA, collateralB] const largestIndex = collateralA.amount > collateralB.amount ? 0 : 1 const largestCollateral = getLargestCollateral(assets, prices) - expect(largestCollateral).toBe(assets[largestIndex].denom) + expect(largestCollateral.denom).toBe(assets[largestIndex].denom) }), test('Can get largest debt correctly', () => { const debtA: Debt = { ...generateRandomAsset(), uncollateralised: false } @@ -24,8 +24,8 @@ describe('Liquidation Tx Generator Tests..', () => { prices.set(debtB.denom, new BigNumber(2)) const assets = [debtA, debtB] const largestIndex = debtA.amount > debtB.amount ? 0 : 1 - const largestCollateral = getLargestDebt(assets, prices).denom + const largestDebt = getLargestDebt(assets, prices).denom - expect(largestCollateral).toBe(assets[largestIndex].denom) + expect(largestDebt).toBe(assets[largestIndex].denom) }) }) diff --git a/test/query/contractQuery.test.ts b/test/query/contractQuery.test.ts index 81c9e8a..558fd54 100644 --- a/test/query/contractQuery.test.ts +++ b/test/query/contractQuery.test.ts @@ -1,10 +1,10 @@ import { Market } from 'marsjs-types/mars-red-bank/MarsRedBank.types' -import { ContractQuery } from '../../src/query/contractQuery' +import { ChainQuery } from '../../src/query/chainQuery' describe('Contract Query Tests', () => { - let contractQuery: ContractQuery + let contractQuery: ChainQuery beforeAll(() => { - contractQuery = new ContractQuery( + contractQuery = new ChainQuery( 'https://neutron-rest.cosmos-apis.com/', process.env.APIKEY!, // TODO put this somewhere better @@ -57,6 +57,12 @@ describe('Contract Query Tests', () => { expect(positions.account_id).toBe('391') expect(positions.account_kind).toBe('default') }) + it('Can query tokens for account correctly', async () => { + let tokens = await contractQuery.queryAccountsForAddress( + 'neutron1ncrjuggwa6x9k9g6a7tsk4atmkhvlq58v8gh5n', + ) + expect(tokens.tokens.length).toBe(2) + }) }) export {} diff --git a/test/redbank/unit/redbankExecutor.test.ts b/test/redbank/unit/redbankExecutor.test.ts index 397a55c..0ccb17e 100644 --- a/test/redbank/unit/redbankExecutor.test.ts +++ b/test/redbank/unit/redbankExecutor.test.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { calculateCollateralRatio, calculateLiquidationBonus, @@ -25,12 +26,12 @@ describe('Redbank Executor Tests', () => { { denom: 'untrn', amount: '1200' }, ] - const prices = new Map([ - ['uosmo', 3], - ['ujake', 1], - ['uatom', 8.2], - ['uusdc', 8.5], - ['untrn', 5.5], + const prices = new Map([ + ['uosmo', new BigNumber(3)], + ['ujake', new BigNumber(1)], + ['uatom', new BigNumber(8.2)], + ['uusdc', new BigNumber(8.5)], + ['untrn', new BigNumber(5.5)], ]) //@ts-ignore @@ -69,7 +70,6 @@ describe('Redbank Executor Tests', () => { targetHealthFactor, //@ts-ignore debts, - // @ts-ignore collaterals, assetLts, liquidationBonus, diff --git a/test/rover/mocks/stateMock.ts b/test/rover/mocks/stateMock.ts index 9f12ef6..53b2221 100644 --- a/test/rover/mocks/stateMock.ts +++ b/test/rover/mocks/stateMock.ts @@ -1,17 +1,29 @@ import { + Addr, Coin, PerpPosition, Positions, + VaultPositionValue, } from 'marsjs-types/mars-credit-manager/MarsCreditManager.types' import { MarketInfo } from '../../../src/rover/types/MarketInfo' import BigNumber from 'bignumber.js' import { InterestRateModel } from 'marsjs-types/mars-red-bank/MarsRedBank.types' +import { + AssetParamsBaseForAddr, + CmSettingsForAddr, + LiquidationBonus, + PerpParams, + RedBankSettings, +} from 'marsjs-types/mars-params/MarsParams.types' +import { HealthData, HealthValuesResponse } from 'mars-liquidation-node' +import { compute_health_js, HealthComputer } from 'mars-rover-health-computer-node' export class StateMock { constructor( public neutralDenom: string, public account: Positions, public markets: Map, + public assetParams: Map, public prices: Map, ) {} @@ -21,7 +33,9 @@ export class StateMock { public setUserDebts(debts: Coin[]): void { this.account.debts = debts.map((debt) => { - return { ...debt, shares: '0' } + let object = { ...debt, shares: debt.amount } + console.log(JSON.stringify(object)) + return object }) } @@ -29,6 +43,36 @@ export class StateMock { this.account.perps = perps } + public getHealth(): HealthData { + let hc: HealthComputer = { + kind: this.account.account_kind, + positions: this.account, + asset_params: Object.fromEntries(this.assetParams), + oracle_prices: Object.fromEntries(this.prices), + perps_data: defaultPerpsData, + vaults_data: defaultVaultsData, + } + + let healthResponse: HealthValuesResponse = compute_health_js(hc) + + const accountNetValue = new BigNumber(healthResponse.total_collateral_value) + .minus(healthResponse.total_debt_value) + .toFixed(0) + const collateralizationRatio = + healthResponse.total_debt_value === '0' + ? new BigNumber(100000000) // Instead of `infinity` we use a very high number + : new BigNumber(healthResponse.total_collateral_value) + .dividedBy(new BigNumber(healthResponse.total_debt_value)) + .toFixed(0) + + return { + liquidation_health_factor: healthResponse.liquidation_health_factor, + account_net_value: accountNetValue, + collateralization_ratio: collateralizationRatio, + perps_pnl_loss: healthResponse.perps_pnl_loss, + } + } + public static default(): StateMock { return new StateMock( 'uusd', @@ -44,16 +88,77 @@ export class StateMock { }, new Map([ ['uusd', { ...defaultMarket, denom: 'uusd' }], - ['atom', { ...defaultMarket, denom: 'uatom' }], + ['uatom', { ...defaultMarket, denom: 'uatom' }], + ]), + new Map([ + ['uusd', { ...defaultAssetParams, denom: 'uusd' }], + ['uatom', { ...defaultAssetParams, denom: 'uatom' }], ]), new Map([ ['uusd', new BigNumber(1)], ['uatom', new BigNumber(10)], + ['ubtc', new BigNumber(100)], ]), ) } } +export const defaultVaultsData = { + vault_values: new Map(), + vault_configs: new Map(), +} + +export const defaultPerpParams: PerpParams = { + denom: 'ubtc', + enabled: true, + liquidation_threshold: '0.85', + max_funding_velocity: '0.1', + max_loan_to_value: '0.8', + max_long_oi_value: '100000000000', + max_net_oi_value: '100000000000', + max_position_value: '100000000000', + max_short_oi_value: '100000000000', + min_position_value: '100000000000', + opening_fee_rate: '0.01', + skew_scale: '100000000000', + closing_fee_rate: '0.01', +} + +export const defaultPerpsData = { + params: Object.fromEntries(new Map([['ubtc', defaultPerpParams]])), +} + +export const defaultCmSettingsForAddr: CmSettingsForAddr = { + whitelisted: true, + hls: undefined, + withdraw_enabled: true, +} + +export const defaultLiquidationBonus: LiquidationBonus = { + min_lb: '0.1', + starting_lb: '0', + max_lb: '0.3', + slope: '0.1', +} + +export const defaultRedbankSettings: RedBankSettings = { + borrow_enabled: true, + deposit_enabled: true, + withdraw_enabled: true, +} + +export const defaultAssetParams: AssetParamsBaseForAddr = { + close_factor: '0.5', + credit_manager: defaultCmSettingsForAddr, + denom: 'uatom', + deposit_cap: '100000000000', + liquidation_bonus: defaultLiquidationBonus, + liquidation_threshold: '0.85', + max_loan_to_value: '0.8', + protocol_liquidation_fee: '0.25', + red_bank: defaultRedbankSettings, +} + export const defaultPerpPosition: PerpPosition = { denom: 'uatom', base_denom: 'uusd', @@ -71,10 +176,10 @@ export const defaultPerpPosition: PerpPosition = { pnl: '0', price_pnl: '0', }, - current_exec_price: '0', - current_price: '0', - entry_exec_price: '0', - entry_price: '0', + current_exec_price: '100', + current_price: '100', + entry_exec_price: '100', + entry_price: '100', size: '10', } @@ -83,8 +188,6 @@ const defaultMarket: MarketInfo = { borrow_index: '0', borrow_rate: '0', collateral_total_scaled: '0', - collateral_total_amount: '0', - debt_total_amount: '0', debt_total_scaled: '0', denom: 'uusd', indexes_last_updated: 0, @@ -92,5 +195,4 @@ const defaultMarket: MarketInfo = { liquidity_index: '0', liquidity_rate: '0', reserve_factor: '0', - utilization_rate: '0', } diff --git a/test/rover/unit/LiquidationActionGenerator.test.ts b/test/rover/unit/LiquidationActionGenerator.test.ts index 1f2221d..dd7b987 100644 --- a/test/rover/unit/LiquidationActionGenerator.test.ts +++ b/test/rover/unit/LiquidationActionGenerator.test.ts @@ -28,7 +28,7 @@ describe('Liquidation Action Generator Tests', () => { mock.setUserDebts([ { denom: 'uatom', - amount: '80', + amount: '86', }, ]) @@ -65,6 +65,8 @@ describe('Liquidation Action Generator Tests', () => { mock.account, mock.prices, mock.markets, + mock.assetParams, + mock.getHealth(), mock.neutralDenom, ) }) @@ -75,11 +77,10 @@ describe('Liquidation Action Generator Tests', () => { // @ts-ignore let denom: String = actions[0].borrow.denom - expect(amount).toBe('80') + expect(amount).toBe('43') expect(denom).toBe('uatom') }) it('Action 1; Should select deposit usd collateral', () => { - console.log(actions[1]) // @ts-ignore let denom: String = actions[1].liquidate.request.deposit @@ -91,7 +92,7 @@ describe('Liquidation Action Generator Tests', () => { expect(debtCoin.denom).toBe('uatom') // TODO check correct debt here - expect(debtCoin.amount).toBe('80') + expect(debtCoin.amount).toBe('43') }) it('Action 2; Should swap all won usd collateral to atom', () => { @@ -138,7 +139,7 @@ describe('Liquidation Action Generator Tests', () => { mock.setUserDeposits([ { denom: 'uusd', - amount: '1000', + amount: '750', }, ]) @@ -186,6 +187,8 @@ describe('Liquidation Action Generator Tests', () => { mock.account, mock.prices, mock.markets, + mock.assetParams, + mock.getHealth(), mock.neutralDenom, ) }) @@ -196,7 +199,7 @@ describe('Liquidation Action Generator Tests', () => { // @ts-ignore let denom: String = actions[0].borrow.denom - expect(amount).toBe('60') + expect(amount).toBe('30') expect(denom).toBe('uatom') }) it('Action 1; Should select deposit usd collateral', () => { @@ -210,7 +213,7 @@ describe('Liquidation Action Generator Tests', () => { let debtCoin: Coin = actions[1].liquidate.debt_coin expect(debtCoin.denom).toBe('uatom') - expect(debtCoin.amount).toBe('60') + expect(debtCoin.amount).toBe('30') }) it('Action 2; Should swap all won usd collateral to atom', () => { @@ -269,7 +272,7 @@ describe('Liquidation Action Generator Tests', () => { }, { denom: 'uusd', - amount: '450', // 450 * 1 = 450 + amount: '500', // 500 * 1 = 500 }, ]) @@ -279,6 +282,8 @@ describe('Liquidation Action Generator Tests', () => { mock.account, mock.prices, mock.markets, + mock.assetParams, + mock.getHealth(), mock.neutralDenom, ) }) @@ -289,7 +294,7 @@ describe('Liquidation Action Generator Tests', () => { // @ts-ignore let denom: String = actions[0].borrow.denom - expect(amount).toBe('450') + expect(amount).toBe('250') expect(denom).toBe('uusd') }) it('Should pick the usd collateral', () => { @@ -304,7 +309,7 @@ describe('Liquidation Action Generator Tests', () => { expect(debtCoin.denom).toBe('uusd') // TODO check correct debt here - expect(debtCoin.amount).toBe('450') + expect(debtCoin.amount).toBe('250') }) it('Should not do any swap of the collateral', () => { @@ -343,11 +348,16 @@ describe('Liquidation Action Generator Tests', () => { { ...defaultPerpPosition, denom: 'ubtc', + size: '100', base_denom: 'uusd', unrealized_pnl: { ...defaultPerpPosition.unrealized_pnl, - pnl: '-100', + pnl: '-600', + price_pnl: '-600', }, + entry_price: '100', + current_price: '94', + current_exec_price: '94', }, ]) @@ -357,6 +367,8 @@ describe('Liquidation Action Generator Tests', () => { mock.account, mock.prices, mock.markets, + mock.assetParams, + mock.getHealth(), mock.neutralDenom, ) }) @@ -367,24 +379,23 @@ describe('Liquidation Action Generator Tests', () => { // @ts-ignore let denom: String = actions[0].borrow.denom - expect(amount).toBe('100') + expect(amount).toBe('300') expect(denom).toBe('uusd') }) it('Should pick the usd collateral', () => { - console.log(actions[1]) // @ts-ignore let denom: String = actions[1].liquidate.request.deposit expect(denom).toBe('uusd') }) - it('Should repay all negative pnl', () => { + it('Should repay negative pnl', () => { // @ts-ignore let debtCoin: Coin = actions[1].liquidate.debt_coin expect(debtCoin.denom).toBe('uusd') - expect(debtCoin.amount).toBe('100') + expect(debtCoin.amount).toBe('300') }) it('Should not do any swap of the collateral', () => { @@ -416,18 +427,22 @@ describe('Liquidation Action Generator Tests', () => { }, ]) - // Make usd larger debt mock.setUserDebts([]) mock.setUserPerpsPositions([ { ...defaultPerpPosition, denom: 'ubtc', + size: '100', base_denom: 'uusd', unrealized_pnl: { ...defaultPerpPosition.unrealized_pnl, pnl: '-1100', + price_pnl: '-1100', }, + entry_price: '100', + current_price: '89', + current_exec_price: '89', }, ]) @@ -437,6 +452,8 @@ describe('Liquidation Action Generator Tests', () => { mock.account, mock.prices, mock.markets, + mock.assetParams, + mock.getHealth(), mock.neutralDenom, ) }) @@ -447,12 +464,11 @@ describe('Liquidation Action Generator Tests', () => { // @ts-ignore let denom: String = actions[0].borrow.denom - expect(amount).toBe('1100') + expect(amount).toBe('550') expect(denom).toBe('uusd') }) it('Should pick the usd collateral', () => { - console.log(actions[1]) // @ts-ignore let denom: String = actions[1].liquidate.request.deposit @@ -464,7 +480,7 @@ describe('Liquidation Action Generator Tests', () => { let debtCoin: Coin = actions[1].liquidate.debt_coin expect(debtCoin.denom).toBe('uusd') - expect(debtCoin.amount).toBe('1100') + expect(debtCoin.amount).toBe('550') }) it('Should not do any swap of the collateral', () => { @@ -486,3 +502,5 @@ describe('Liquidation Action Generator Tests', () => { }) }) }) + +// TODO perp case When borrow amount is 0 diff --git a/test/rover/unit/executor.test.ts b/test/rover/unit/executor.test.ts index 3cf1d93..fa63f53 100644 --- a/test/rover/unit/executor.test.ts +++ b/test/rover/unit/executor.test.ts @@ -28,8 +28,7 @@ describe('Rover Executor Tests', () => { // calculate the debts, deposits const details = calculatePositionStateAfterPerpClosure(positions, baseDenom) - expect(details.debts[0].amount).toBe('0') - expect(details.debts[0].denom).toBe('uusd') + expect(details.debts.length).toBe(0) expect(details.deposits[0].amount).toBe('100') expect(details.deposits[0].denom).toBe('uusd') }), @@ -205,10 +204,8 @@ describe('Rover Executor Tests', () => { // We first deduct from deposits, then unlend, then borrow // Because we have 1k lend and 1 deposit with 200 negative pnl, we should have // 900 lend and 0 deposit - expect(details.debts[0].amount).toBe('0') - expect(details.debts[0].denom).toBe('uusd') - expect(details.deposits[0].amount).toBe('0') - expect(details.deposits[0].denom).toBe('uusd') + expect(details.debts.length).toBe(0) + expect(details.deposits.length).toBe(0) expect(details.lends[0].amount).toBe('900') expect(details.lends[0].denom).toBe('uusd') }), @@ -244,10 +241,8 @@ describe('Rover Executor Tests', () => { // 0 lend and 0 deposit and 100 debt expect(details.debts[0].amount).toBe('100') expect(details.debts[0].denom).toBe('uusd') - expect(details.deposits[0].amount).toBe('0') - expect(details.deposits[0].denom).toBe('uusd') - expect(details.lends[0].amount).toBe('0') - expect(details.lends[0].denom).toBe('uusd') + expect(details.deposits.length).toBe(0) + expect(details.lends.length).toBe(0) }) }) }) diff --git a/yarn.lock b/yarn.lock index dda1c18..8dcab07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5210,6 +5210,16 @@ map-obj@^4.3.0: resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +mars-liquidation-node@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mars-liquidation-node/-/mars-liquidation-node-1.0.0.tgz#3751c4f1271b38dbd8f72ba8709291740ccc5427" + integrity sha512-dSfUUvENgdFN1lsLOVOlzc7X9A3kf1evBggy5vscvE9ugtGRMqa/tLhM3DwVajilc6hE0J5KQ/5OEc6CWYZF6A== + +mars-rover-health-computer-node@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/mars-rover-health-computer-node/-/mars-rover-health-computer-node-2.2.0.tgz#32a443077c97a37eee9dc2e2d1195ba4107a76ba" + integrity sha512-wPJHY/E7jIURgfNkokTUR25fax7jd9+ew0gLht/BDLPPjQQtWRwthD1y8Le0t0AUq5/77tRLdeBM6wTjM5EBDA== + marsjs-types@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/marsjs-types/-/marsjs-types-2.0.2.tgz#7919fd0ba22685082d4907ebce842364d54194a4"