diff --git a/package.json b/package.json index 753287dfe..e1ec5012f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/interbtc-api.ts b/src/interbtc-api.ts index fb048f301..82a9b2827 100644 --- a/src/interbtc-api.ts +++ b/src/interbtc-api.ts @@ -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"; @@ -71,6 +72,7 @@ export interface InterBtcApi { readonly account: AddressOrPair | undefined; getGovernanceCurrency(): GovernanceCurrency; getWrappedCurrency(): WrappedCurrency; + getRelayChainCurrency(): Currency; disconnect(): Promise; } @@ -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 { return this.api.disconnect(); } diff --git a/src/parachain/index.ts b/src/parachain/index.ts index 8a4cad4c2..1fa7fb4aa 100644 --- a/src/parachain/index.ts +++ b/src/parachain/index.ts @@ -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"; diff --git a/src/parachain/loans.ts b/src/parachain/loans.ts index e91dd213a..516c041c3 100644 --- a/src/parachain/loans.ts +++ b/src/parachain/loans.ts @@ -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, @@ -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"; @@ -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; + /** + * @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 { @@ -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][]> { - 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( @@ -286,15 +296,9 @@ export class DefaultLoansAPI implements LoansAPI { } async getLendTokens(): Promise { - 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) ); } @@ -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], }); } @@ -337,13 +341,12 @@ export class DefaultLoansAPI implements LoansAPI { async _getLendPosition( accountId: AccountId, underlyingCurrency: CurrencyExt, - underlyingCurrencyId: InterbtcPrimitivesCurrencyId, lendTokenId: InterbtcPrimitivesCurrencyId ): Promise { const [underlyingCurrencyAmount] = await this.getLendPositionAmounts( accountId, lendTokenId, - underlyingCurrencyId + newCurrencyId(this.api, underlyingCurrency) ); // Returns null if position does not exist if (underlyingCurrencyAmount.eq(0)) { @@ -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 { + async _getBorrowPosition(accountId: AccountId, underlyingCurrency: CurrencyExt): Promise { + 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()); @@ -398,24 +398,17 @@ export class DefaultLoansAPI implements LoansAPI { getSinglePosition: ( accountId: AccountId, underlyingCurrency: CurrencyExt, - underlyingCurrencyId: InterbtcPrimitivesCurrencyId, lendTokenId: InterbtcPrimitivesCurrencyId ) => Promise ): Promise> { - 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 >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; } async getLendPositionsOfAccount(accountId: AccountId): Promise> { @@ -614,16 +607,6 @@ export class DefaultLoansAPI implements LoansAPI { return newMonetaryAmount(amount, rewardCurrency); } - async _getExchangeRate(fromCurrency: CurrencyExt): Promise> { - 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 @@ -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. @@ -675,10 +658,10 @@ export class DefaultLoansAPI implements LoansAPI { } async getLoanAssets(): Promise> { - 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) ) ); diff --git a/src/parachain/oracle.ts b/src/parachain/oracle.ts index 9ee824399..d3f1bba85 100644 --- a/src/parachain/oracle.ts +++ b/src/parachain/oracle.ts @@ -12,6 +12,7 @@ import { createFeeEstimationOracleKey, decodeFixedPointType, encodeUnsignedFixedPoint, + isCurrencyEqual, storageKeyToNthInner, unwrapRawExchangeRate, } from "../utils"; @@ -93,6 +94,14 @@ export class DefaultOracleAPI implements OracleAPI { ) { } async getExchangeRate(currency: CurrencyExt): Promise> { + // KBTC / IBTC have an exchange rate of one + if (isCurrencyEqual(currency, this.wrappedCurrency)) { + return new ExchangeRate( + currency, + currency, + new Big(1), + ); + } const oracleKey = createExchangeRateOracleKey(this.api, currency); const encodedRawRate = unwrapRawExchangeRate(await this.api.query.oracle.aggregate(oracleKey)); diff --git a/src/types/loans.ts b/src/types/loans.ts index 309ff1d71..6c698f692 100644 --- a/src/types/loans.ts +++ b/src/types/loans.ts @@ -74,7 +74,7 @@ type AccountLiquidity = { type UndercollateralizedPosition = { accountId: AccountId; shortfall: MonetaryAmount; - collateralPositions: Array; + collateralPositions: Array; borrowPositions: Array; }; diff --git a/test/integration/parachain/staging/sequential/loans.test.ts b/test/integration/parachain/staging/sequential/loans.test.ts index 1ac878f23..928d7b1d1 100644 --- a/test/integration/parachain/staging/sequential/loans.test.ts +++ b/test/integration/parachain/staging/sequential/loans.test.ts @@ -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; @@ -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;