From 4e4b3d520d1b16c277650808e1f4c652a2d0d243 Mon Sep 17 00:00:00 2001 From: Filippo Date: Fri, 2 Feb 2024 11:18:07 +0100 Subject: [PATCH] 147-Fetch-outstandingDebt-with-runtime-call * feat: add runtime call * feat: fetch outstandingDebt with runtime call Fixes #147 * feat: initialise loan props * chore: loan logging * chore: formatting --- schema.graphql | 28 ++++++++- src/chaintypes.ts | 46 +++++++++++++++ src/helpers/types.ts | 19 +++++- src/mappings/handlers/blockHandlers.ts | 17 +++--- src/mappings/services/loanService.test.ts | 20 ------- src/mappings/services/loanService.ts | 31 +++++----- src/mappings/services/poolService.ts | 70 ++++++++++++++--------- 7 files changed, 155 insertions(+), 76 deletions(-) diff --git a/schema.graphql b/schema.graphql index 0ca13909..e14b031d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -11,7 +11,6 @@ type Pool @entity { type: String! @index isActive: Boolean! @index - createdAt: Date createdAtBlockNumber: Int @@ -309,7 +308,6 @@ type Loan @entity { probabilityOfDefault: BigInt lossGivenDefault: BigInt discountRate: BigInt - maturityDate: Date interestRatePerSec: BigInt @@ -320,7 +318,19 @@ type Loan @entity { isActive: Boolean! @index status: LoanStatus! + outstandingPrincipal: BigInt + outstandingInterest: BigInt outstandingDebt: BigInt + presentValue: BigInt + actualMaturityDate: Date + timeToMaturity: Int + actualOriginationDate: Date + writeOffPercentage: BigInt + totalBorrowed: BigInt + totalRepaid: BigInt + totalRepaidPrincipal: BigInt + totalRepaidInterest: BigInt + totalRepaidUnscheduled: BigInt borrowedAmountByPeriod: BigInt repaidAmountByPeriod: BigInt @@ -339,7 +349,19 @@ type LoanSnapshot @entity { blockNumber: Int! periodStart: Date! @index + outstandingPrincipal: BigInt + outstandingInterest: BigInt outstandingDebt: BigInt + presentValue: BigInt + actualMaturityDate: Date + timeToMaturity: Int + actualOriginationDate: Date + writeOffPercentage: BigInt + totalBorrowed: BigInt + totalRepaid: BigInt + totalRepaidPrincipal: BigInt + totalRepaidInterest: BigInt + totalRepaidUnscheduled: BigInt borrowedAmountByPeriod: BigInt repaidAmountByPeriod: BigInt @@ -392,4 +414,4 @@ type CurrencyBalance @entity { type Blockchain @entity { id: ID! #EVM chainId -} \ No newline at end of file +} diff --git a/src/chaintypes.ts b/src/chaintypes.ts index c93086c5..d8123fdf 100644 --- a/src/chaintypes.ts +++ b/src/chaintypes.ts @@ -3,6 +3,52 @@ import type { OverrideBundleDefinition } from '@polkadot/types/types' /* eslint-disable sort-keys */ const definitions: OverrideBundleDefinition = { + types: [ + { + minmax: [undefined, undefined], + types: { + ActiveLoanInfo: { + activeLoan: 'PalletLoansEntitiesLoansActiveLoan', + presentValue: 'Balance', + outstandingPrincipal: 'Balance', + outstandingInterest: 'Balance', + }, + }, + }, + ], + runtime: { + LoansApi: [ + { + methods: { + portfolio: { + description: 'Get active pool loan', + params: [ + { + name: 'pool_id', + type: 'u64', + }, + ], + type: 'Vec<(u64, ActiveLoanInfo)>', + }, + portfolio_loan: { + description: 'Get active pool loan', + params: [ + { + name: 'pool_id', + type: 'u64', + }, + { + name: 'loan_id', + type: 'u64', + }, + ], + type: 'Option', + }, + }, + version: 1, + }, + ], + }, rpc: { pools: { trancheTokenPrices: { diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 55788c8c..2a328a8c 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -1,7 +1,7 @@ //find out types: const a = createType(api.registry, '[u8;32]', 18) -import { AugmentedRpc, PromiseRpcResult } from '@polkadot/api/types' +import { AugmentedCall, AugmentedRpc, PromiseRpcResult } from '@polkadot/api/types' import { Enum, Null, Struct, u128, u32, u64, U8aFixed, Option, Vec, Bytes } from '@polkadot/types' -import { AccountId32, Perquintill } from '@polkadot/types/interfaces' +import { AccountId32, Perquintill, Balance } from '@polkadot/types/interfaces' import { ITuple, Observable } from '@polkadot/types/types' export interface PoolDetails extends Struct { @@ -162,6 +162,13 @@ export interface LoanInfoCreated extends Struct { } } +export interface LoanInfoActivePortfolio extends Struct { + activeLoan: LoanInfoActive, + presentValue: Balance, + outstandingPrincipal: Balance, + outstandingInterest: Balance, +} + export interface LoanInfoActive extends Struct { schedule: LoanRepaymentSchedule collateral: ITuple<[u64, u128]> @@ -339,7 +346,7 @@ export type InvestOrdersCollectedEvent = ITuple< who: AccountId32, processedOrders: Vec, collection: InvestCollection, - outcome: Enum + outcome: Enum, ] > export type RedeemOrdersCollectedEvent = ITuple< @@ -364,3 +371,9 @@ export type ExtendedRpc = typeof api.rpc & { trancheTokenPrices: PromiseRpcResult Observable>>> } } + +export type ExtendedCall = typeof api.call & { + loansApi: { + portfolio: AugmentedCall<'promise', (poolId: string) => Observable>>> + } +} diff --git a/src/mappings/handlers/blockHandlers.ts b/src/mappings/handlers/blockHandlers.ts index 1cb931ac..cab8d02f 100644 --- a/src/mappings/handlers/blockHandlers.ts +++ b/src/mappings/handlers/blockHandlers.ts @@ -5,6 +5,7 @@ import { stateSnapshotter } from '../../helpers/stateSnapshot' import { SNAPSHOT_INTERVAL_SECONDS } from '../../config' import { PoolService } from '../services/poolService' import { TrancheService } from '../services/trancheService' +import { LoanService } from '../services/loanService' const timekeeper = TimekeeperService.init() @@ -45,16 +46,14 @@ async function _handleBlock(block: SubstrateBlock): Promise { await tranche.save() } - const activeLoanData = await pool.getActiveLoanData() - // TODO: Reinclude outstanding debt calculation - // for (const loanId in activeLoanData) { - // const loan = await LoanService.getById(pool.id, loanId) - // const { normalizedAcc, interestRate } = activeLoanData[loanId] - // await loan.updateOutstandingDebt(normalizedAcc, interestRate) - // await loan.save() + const activeLoanData = await pool.getPortfolio() + for (const loanId in activeLoanData) { + const loan = await LoanService.getById(pool.id, loanId) + await loan.updateActiveLoanData(activeLoanData[loanId]) + await loan.save() - // if (loan.maturityDate < block.timestamp) await pool.increaseDebtOverdue(loan.outstandingDebt) - // } + if (loan.actualMaturityDate < block.timestamp) await pool.increaseDebtOverdue(loan.outstandingDebt) + } await pool.updateNumberOfActiveLoans(BigInt(Object.keys(activeLoanData).length)) await pool.save() diff --git a/src/mappings/services/loanService.test.ts b/src/mappings/services/loanService.test.ts index 058e311c..29ce5edc 100644 --- a/src/mappings/services/loanService.test.ts +++ b/src/mappings/services/loanService.test.ts @@ -1,6 +1,3 @@ -import { bnToBn, nToBigInt } from '@polkadot/util' -import { RAY, WAD } from '../../config' - import { LoanService } from './loanService' const poolId = '1111111111' @@ -8,18 +5,8 @@ const loanId = 'ABCD' const nftClassId = BigInt(1) const nftItemId = BigInt(2) const timestamp = new Date() -const accumulatedRate = nToBigInt(RAY.muln(2)) -const normalizedAcc = nToBigInt(WAD.muln(100)) -const interestRate = nToBigInt(WAD) const metadata = 'AAAAAA' -const outstandingDebt = nToBigInt(bnToBn(normalizedAcc).mul(bnToBn(accumulatedRate)).div(RAY)) - -api.query['interestAccrual'] = { - rates: jest.fn(() => [{ interestRatePerSec: { toBigInt: () => interestRate }, accumulatedRate }]), - // eslint-disable-next-line @typescript-eslint/no-explicit-any -} as any - api.query['uniques'] = { instanceMetadataOf: jest.fn(() => ({ isNone: false, @@ -54,10 +41,3 @@ describe('Given a new loan, when initialised', () => { expect(store.set).toHaveBeenCalledWith('Loan', `${poolId}-${loanId}`, expect.anything()) }) }) - -describe('Given an existing loan, ', () => { - test.skip('when a snapshot is taken, then the outstanding debt is computed corrrectly', async () => { - await loan.updateOutstandingDebt(normalizedAcc, interestRate) - expect(loan.outstandingDebt).toBe(outstandingDebt) - }) -}) diff --git a/src/mappings/services/loanService.ts b/src/mappings/services/loanService.ts index 7634ac57..3d4b56c1 100644 --- a/src/mappings/services/loanService.ts +++ b/src/mappings/services/loanService.ts @@ -1,19 +1,25 @@ -import { Option, Vec } from '@polkadot/types' +import { Option } from '@polkadot/types' import { bnToBn, nToBigInt } from '@polkadot/util' -import { RAY, WAD } from '../../config' -import { InterestAccrualRateDetails, NftItemMetadata } from '../../helpers/types' +import { WAD } from '../../config' +import { NftItemMetadata } from '../../helpers/types' import { Loan, LoanStatus } from '../../types' - -const SECONDS_PER_YEAR = bnToBn('3600').muln(24).muln(365) +import { ActiveLoanData } from './poolService' export class LoanService extends Loan { static init(poolId: string, loanId: string, nftClassId: bigint, nftItemId: bigint, timestamp: Date) { logger.info(`Initialising loan ${loanId} for pool ${poolId}`) const loan = new this(`${poolId}-${loanId}`, timestamp, nftClassId, nftItemId, poolId, false, LoanStatus.CREATED) + loan.outstandingPrincipal = BigInt(0) + loan.outstandingInterest = BigInt(0) loan.outstandingDebt = BigInt(0) - loan.borrowedAmountByPeriod = BigInt(0) - loan.repaidAmountByPeriod = BigInt(0) + loan.presentValue = BigInt(0) + loan.writeOffPercentage = BigInt(0) + loan.totalBorrowed = BigInt(0) + loan.totalRepaid = BigInt(0) + loan.totalRepaidPrincipal = BigInt(0) + loan.totalRepaidInterest = BigInt(0) + loan.totalRepaidUnscheduled = BigInt(0) return loan } @@ -47,6 +53,7 @@ export class LoanService extends Loan { } public updateLoanSpecs(decodedLoanSpecs: LoanSpecs) { + logger.info(`Updating loan specs for ${this.id}`) Object.assign(this, decodedLoanSpecs) } @@ -62,14 +69,8 @@ export class LoanService extends Loan { this.status = LoanStatus.CLOSED } - public async updateOutstandingDebt(normalizedAcc: bigint, interestRate: bigint) { - const interestRatePerSec = nToBigInt(bnToBn(interestRate).div(SECONDS_PER_YEAR).add(RAY)) - logger.info(`Calculated IRS: ${interestRatePerSec.toString()}`) - const rateDetails = await api.query.interestAccrual.rates>() - const { accumulatedRate } = rateDetails.find( - (rateDetails) => rateDetails.interestRatePerSec.toBigInt() === interestRatePerSec - ) - this.outstandingDebt = nToBigInt(bnToBn(normalizedAcc).mul(bnToBn(accumulatedRate)).div(RAY)) + public async updateActiveLoanData(activeLoanData: ActiveLoanData[keyof ActiveLoanData]) { + Object.assign(this, activeLoanData) logger.info(`Updating outstanding debt for loan: ${this.id} to ${this.outstandingDebt.toString()}`) } diff --git a/src/mappings/services/poolService.ts b/src/mappings/services/poolService.ts index 09f0b8f8..504a8c68 100644 --- a/src/mappings/services/poolService.ts +++ b/src/mappings/services/poolService.ts @@ -1,8 +1,7 @@ -import { Option, u64, u128, Vec } from '@polkadot/types' -import { ITuple } from '@polkadot/types/types' +import { Option, u128, Vec } from '@polkadot/types' import { bnToBn, nToBigInt } from '@polkadot/util' import { paginatedGetter } from '../../helpers/paginatedGetter' -import { ExtendedRpc, LoanInfoActive, NavDetails, PoolDetails, PoolMetadata, TrancheDetails } from '../../helpers/types' +import { ExtendedCall, ExtendedRpc, NavDetails, PoolDetails, PoolMetadata, TrancheDetails } from '../../helpers/types' import { Pool } from '../../types' export class PoolService extends Pool { @@ -13,9 +12,9 @@ export class PoolService extends Pool { static async getOrSeed(poolId: string, saveSeed = true) { let pool = await this.getById(poolId) - if(!pool) { + if (!pool) { pool = this.seed(poolId) - if(saveSeed) await pool.save() + if (saveSeed) await pool.save() } return pool } @@ -180,25 +179,30 @@ export class PoolService extends Pool { return tranches.reduce((obj, data, index) => ({ ...obj, [ids[index].toHex()]: { index, data } }), {}) } - public async getActiveLoanData() { - logger.info(`Querying active loan data for pool: ${this.id}`) - const loanDetails = await api.query.loans.activeLoans>>(this.id) - const activeLoanData = loanDetails.reduce( - (last, current) => ({ - ...last, - [current[0].toString()]: { - normalizedAcc: current[1].pricing?.isInternal - ? current[1].pricing?.asInternal.interest.normalizedAcc.toBigInt() - : null, - interestRate: - current[1].pricing?.isInternal && current[1].pricing?.asInternal.interest.interestRate.isFixed - ? current[1].pricing?.asInternal.interest.interestRate.asFixed.ratePerYear.toBigInt() - : null, - }, - }), - {} - ) - return activeLoanData + public async getPortfolio(): Promise { + const apiCall = api.call as ExtendedCall + const portfolioData = await apiCall.loansApi.portfolio(this.id) + return portfolioData.reduce((obj, current) => { + const totalRepaid = current[1].activeLoan.totalRepaid + const maturityDate = new Date(current[1].activeLoan.schedule.maturity.asFixed.date.toNumber() * 1000) + obj[current[0].toString(10)] = { + outstandingPrincipal: current[1].outstandingPrincipal.toBigInt(), + outstandingInterest: current[1].outstandingInterest.toBigInt(), + outstandingDebt: current[1].outstandingPrincipal.toBigInt() + current[1].outstandingInterest.toBigInt(), + presentValue: current[1].presentValue.toBigInt(), + actualMaturityDate: new Date(current[1].activeLoan.schedule.maturity.asFixed.date.toNumber() * 1000), + timeToMaturity: Math.round((maturityDate.valueOf() - Date.now().valueOf()) / 1000), + actualOriginationDate: new Date(current[1].activeLoan.originationDate.toNumber() * 1000), + writeOffPercentage: current[1].activeLoan.writeOffPercentage.toBigInt(), + totalBorrowed: current[1].activeLoan.totalBorrowed.toBigInt(), + totalRepaid: + totalRepaid.principal.toBigInt() + totalRepaid.interest.toBigInt() + totalRepaid.unscheduled.toBigInt(), + totalRepaidPrincipal: totalRepaid.principal.toBigInt(), + totalRepaidInterest: totalRepaid.interest.toBigInt(), + totalRepaidUnscheduled: totalRepaid.unscheduled.toBigInt(), + } + return obj + }, {}) } public async getTrancheTokenPrices() { @@ -215,8 +219,22 @@ export class PoolService extends Pool { } } -interface ActiveLoanData { - [loanId: string]: { normalizedAcc: bigint; interestRate: bigint } +export interface ActiveLoanData { + [loanId: string]: { + outstandingPrincipal: bigint, + outstandingInterest: bigint, + outstandingDebt: bigint, + presentValue: bigint, + actualMaturityDate: Date, + timeToMaturity: number + actualOriginationDate: Date, + writeOffPercentage: bigint, + totalBorrowed: bigint, + totalRepaid: bigint + totalRepaidPrincipal: bigint, + totalRepaidInterest: bigint, + totalRepaidUnscheduled: bigint, + } } interface PoolTranches {