Skip to content

Commit

Permalink
Merge pull request #605 from interlay/dan/get-loans-markets
Browse files Browse the repository at this point in the history
feat(loans): get loans markets, refactor usage
  • Loading branch information
daniel-savu authored Mar 14, 2023
2 parents 1996322 + dce9b24 commit b762286
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@interlay/interbtc-api",
"version": "2.0.0",
"version": "2.0.1",
"description": "JavaScript library to interact with interBTC",
"main": "build/src/index.js",
"typings": "build/src/index.d.ts",
Expand Down
8 changes: 8 additions & 0 deletions src/interbtc-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
tokenSymbolToCurrency
} from "./utils";
import { AssetRegistryAPI } from "./parachain/asset-registry";
import { Currency } from "@interlay/monetary-js";
import { AMMAPI, DefaultAMMAPI } from "./parachain/amm";

export * from "./factory";
Expand Down Expand Up @@ -71,6 +72,7 @@ export interface InterBtcApi {
readonly account: AddressOrPair | undefined;
getGovernanceCurrency(): GovernanceCurrency;
getWrappedCurrency(): WrappedCurrency;
getRelayChainCurrency(): Currency;
disconnect(): Promise<void>;
}

Expand Down Expand Up @@ -201,6 +203,12 @@ export class DefaultInterBtcApi implements InterBtcApi {
return tokenSymbolToCurrency(currencyId.asToken);
}

public getRelayChainCurrency(): Currency {
const currencyId = this.api.consts.currency.getRelayChainCurrencyId;
// beware: this call will throw if the currency is not a token!
return tokenSymbolToCurrency(currencyId.asToken);
}

public disconnect(): Promise<void> {
return this.api.disconnect();
}
Expand Down
2 changes: 1 addition & 1 deletion src/parachain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export * from "./transaction";
export * from "./amm/";

// Hacky way of forcing the resolution of these types in test files
export { InterbtcPrimitivesVaultId, VaultRegistryVault, SecurityStatusCode } from "@polkadot/types/lookup";
export { InterbtcPrimitivesVaultId, VaultRegistryVault, SecurityStatusCode, LoansMarket } from "@polkadot/types/lookup";
95 changes: 39 additions & 56 deletions src/parachain/loans.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AccountId } from "@polkadot/types/interfaces";
import { ExchangeRate, MonetaryAmount } from "@interlay/monetary-js";
import { MonetaryAmount } from "@interlay/monetary-js";
import {
BorrowPosition,
CurrencyExt,
Expand Down Expand Up @@ -30,12 +30,9 @@ import {
calculateThreshold,
calculateBorrowLimitBtcChangeFactory,
calculateLtvAndThresholdsChangeFactory,
isCurrencyEqual,
tokenSymbolToCurrency,
newAccountId,
} from "../utils";
import { InterbtcPrimitivesCurrencyId, LoansMarket } from "@polkadot/types/lookup";
import { StorageKey, Option } from "@polkadot/types";
import { TransactionAPI } from "./transaction";
import { OracleAPI } from "./oracle";

Expand Down Expand Up @@ -215,6 +212,10 @@ export interface LoansAPI {
* @returns An `AccountLiquidity` object, which is valid even for accounts that didn't use the loans pallet at all
*/
getLiquidationThresholdLiquidity(accountId: AccountId): Promise<AccountLiquidity>;
/**
* @returns An array of tuples denoting the underlying currency of a market, and the configuration of that market
*/
getLoansMarkets(): Promise<[CurrencyExt, LoansMarket][]>;
}

export class DefaultLoansAPI implements LoansAPI {
Expand All @@ -225,10 +226,19 @@ export class DefaultLoansAPI implements LoansAPI {
private oracleAPI: OracleAPI
) {}

// Wrapped call to make mocks in tests simple.
async getLoansMarketsEntries(): Promise<[StorageKey<[InterbtcPrimitivesCurrencyId]>, Option<LoansMarket>][]> {
const entries = await this.api.query.loans.markets.entries();
return entries.filter((entry) => entry[1].isSome);
async getLoansMarkets(): Promise<[CurrencyExt, LoansMarket][]> {
const entries = (await this.api.query.loans.markets.entries()).filter((entry) => entry[1].isSome);
const parsedMarkets = await Promise.all(
entries.map(async ([key, market]): Promise<[CurrencyExt, LoansMarket]> => {
const underlyingCurrencyId = storageKeyToNthInner(key);
const underlyingCurrency = await currencyIdToMonetaryCurrency(
this.api,
underlyingCurrencyId
);
return [underlyingCurrency, market.unwrap()];
})
);
return parsedMarkets;
}

static getLendTokenFromUnderlyingCurrency(
Expand Down Expand Up @@ -286,15 +296,9 @@ export class DefaultLoansAPI implements LoansAPI {
}

async getLendTokens(): Promise<LendToken[]> {
const marketEntries = await this.getLoansMarketsEntries();

return Promise.all(
marketEntries.map(async ([key, market]) => {
const lendTokenId = market.unwrap().lendTokenId;
const underlyingCurrencyId = storageKeyToNthInner(key);
const underlyingCurrency = await currencyIdToMonetaryCurrency(this.api, underlyingCurrencyId);
return DefaultLoansAPI.getLendTokenFromUnderlyingCurrency(underlyingCurrency, lendTokenId);
})
const marketEntries = await this.getLoansMarkets();
return marketEntries.map(([currency, loansMarket]) =>
DefaultLoansAPI.getLendTokenFromUnderlyingCurrency(currency, loansMarket.lendTokenId)
);
}

Expand All @@ -319,7 +323,7 @@ export class DefaultLoansAPI implements LoansAPI {
undercollateralizedPositions.push({
accountId: borrowers[i],
shortfall: liquidity[i].shortfall,
collateralPositions: collateral[i],
collateralPositions: collateral[i].filter((position) => position.isCollateral),
borrowPositions: borrows[i],
});
}
Expand All @@ -337,13 +341,12 @@ export class DefaultLoansAPI implements LoansAPI {
async _getLendPosition(
accountId: AccountId,
underlyingCurrency: CurrencyExt,
underlyingCurrencyId: InterbtcPrimitivesCurrencyId,
lendTokenId: InterbtcPrimitivesCurrencyId
): Promise<CollateralPosition | null> {
const [underlyingCurrencyAmount] = await this.getLendPositionAmounts(
accountId,
lendTokenId,
underlyingCurrencyId
newCurrencyId(this.api, underlyingCurrency)
);
// Returns null if position does not exist
if (underlyingCurrencyAmount.eq(0)) {
Expand All @@ -369,14 +372,11 @@ export class DefaultLoansAPI implements LoansAPI {
return borrowedAmount.mul(factor).round(0, RoundingMode.RoundUp);
}

async _getBorrowPosition(
accountId: AccountId,
underlyingCurrency: CurrencyExt,
lendTokenId: InterbtcPrimitivesCurrencyId
): Promise<BorrowPosition | null> {
async _getBorrowPosition(accountId: AccountId, underlyingCurrency: CurrencyExt): Promise<BorrowPosition | null> {
const underlyingCurrencyPrimitive = newCurrencyId(this.api, underlyingCurrency);
const [borrowSnapshot, marketStatus] = await Promise.all([
this.api.query.loans.accountBorrows(lendTokenId, accountId),
this.api.rpc.loans.getMarketStatus(lendTokenId),
this.api.query.loans.accountBorrows(underlyingCurrencyPrimitive, accountId),
this.api.rpc.loans.getMarketStatus(underlyingCurrencyPrimitive),
]);

const borrowedAmount = Big(borrowSnapshot.principal.toString());
Expand All @@ -398,24 +398,17 @@ export class DefaultLoansAPI implements LoansAPI {
getSinglePosition: (
accountId: AccountId,
underlyingCurrency: CurrencyExt,
underlyingCurrencyId: InterbtcPrimitivesCurrencyId,
lendTokenId: InterbtcPrimitivesCurrencyId
) => Promise<Position | null>
): Promise<Array<Position>> {
const marketsEntries = await this.getLoansMarketsEntries();
const marketsCurrencies = marketsEntries.map(([key, value]) => [
storageKeyToNthInner(key),
value.unwrap().lendTokenId,
]);

const allMarketsPositions = await Promise.all(
marketsCurrencies.map(async ([underlyingCurrencyId, lendTokenId]) => {
const underlyingCurrency = await currencyIdToMonetaryCurrency(this.api, underlyingCurrencyId);
return getSinglePosition(accountId, underlyingCurrency, underlyingCurrencyId, lendTokenId);
})
);

return <Array<Position>>allMarketsPositions.filter((position) => position !== null);
const marketsEntries = await this.getLoansMarkets();
return (
await Promise.all(
marketsEntries.map(([currency, loansMarket]) => {
return getSinglePosition(accountId, currency, loansMarket.lendTokenId);
})
)
).filter((position) => position !== null) as Array<Position>;
}

async getLendPositionsOfAccount(accountId: AccountId): Promise<Array<CollateralPosition>> {
Expand Down Expand Up @@ -614,16 +607,6 @@ export class DefaultLoansAPI implements LoansAPI {
return newMonetaryAmount(amount, rewardCurrency);
}

async _getExchangeRate(fromCurrency: CurrencyExt): Promise<ExchangeRate<WrappedCurrency, CurrencyExt>> {
const wrappedCurrencyId = this.api.consts.escrowRewards.getWrappedCurrencyId;
const wrappedCurrency = tokenSymbolToCurrency(wrappedCurrencyId.asToken);
if (isCurrencyEqual(fromCurrency, wrappedCurrency)) {
const wrappedCurrencyToBitcoinRate = Big(1);
return new ExchangeRate(wrappedCurrency, wrappedCurrency, wrappedCurrencyToBitcoinRate);
}
return this.oracleAPI.getExchangeRate(fromCurrency);
}

async _getLoanAsset(
underlyingCurrencyId: InterbtcPrimitivesCurrencyId,
marketData: LoansMarket
Expand All @@ -636,7 +619,7 @@ export class DefaultLoansAPI implements LoansAPI {
this._getBorrowApy(underlyingCurrencyId),
this._getTotalLiquidityCapacityAndBorrows(underlyingCurrency, underlyingCurrencyId),
this._getRewardCurrency(),
this._getExchangeRate(underlyingCurrency),
this.oracleAPI.getExchangeRate(underlyingCurrency)
]);

// Format data.
Expand Down Expand Up @@ -675,10 +658,10 @@ export class DefaultLoansAPI implements LoansAPI {
}

async getLoanAssets(): Promise<TickerToData<LoanAsset>> {
const marketsEntries = await this.getLoansMarketsEntries();
const marketsEntries = await this.getLoansMarkets();
const loanAssetsArray = await Promise.all(
marketsEntries.map(([key, marketData]) =>
this._getLoanAsset(storageKeyToNthInner(key), marketData.unwrap())
marketsEntries.map(([currency, loansMarket]) =>
this._getLoanAsset(newCurrencyId(this.api, currency), loansMarket)
)
);

Expand Down
9 changes: 9 additions & 0 deletions src/parachain/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
createFeeEstimationOracleKey,
decodeFixedPointType,
encodeUnsignedFixedPoint,
isCurrencyEqual,
storageKeyToNthInner,
unwrapRawExchangeRate,
} from "../utils";
Expand Down Expand Up @@ -93,6 +94,14 @@ export class DefaultOracleAPI implements OracleAPI {
) { }

async getExchangeRate(currency: CurrencyExt): Promise<ExchangeRate<Bitcoin, CurrencyExt>> {
// KBTC / IBTC have an exchange rate of one
if (isCurrencyEqual(currency, this.wrappedCurrency)) {
return new ExchangeRate<WrappedCurrency, CurrencyExt>(
currency,
currency,
new Big(1),
);
}
const oracleKey = createExchangeRateOracleKey(this.api, currency);

const encodedRawRate = unwrapRawExchangeRate(await this.api.query.oracle.aggregate(oracleKey));
Expand Down
2 changes: 1 addition & 1 deletion src/types/loans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type AccountLiquidity = {
type UndercollateralizedPosition = {
accountId: AccountId;
shortfall: MonetaryAmount<WrappedCurrency>;
collateralPositions: Array<LoanPosition>;
collateralPositions: Array<CollateralPosition>;
borrowPositions: Array<BorrowPosition>;
};

Expand Down
4 changes: 2 additions & 2 deletions test/integration/parachain/staging/sequential/loans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ describe("Loans", () => {

it("should return empty array if no market exists", async () => {
// Mock empty list returned from chain.
sinon.stub(LoansAPI, "getLoansMarketsEntries").returns(Promise.resolve([]));
sinon.stub(LoansAPI, "getLoansMarkets").returns(Promise.resolve([]));

const lendTokens = await LoansAPI.getLendTokens();
expect(lendTokens).to.be.empty;
Expand Down Expand Up @@ -355,7 +355,7 @@ describe("Loans", () => {

it("should return empty object if there are no added markets", async () => {
// Mock empty list returned from chain.
sinon.stub(LoansAPI, "getLoansMarketsEntries").returns(Promise.resolve([]));
sinon.stub(LoansAPI, "getLoansMarkets").returns(Promise.resolve([]));

const loanAssets = await LoansAPI.getLoanAssets();
expect(loanAssets).to.be.empty;
Expand Down

0 comments on commit b762286

Please sign in to comment.