Skip to content

Commit

Permalink
147-Fetch-outstandingDebt-with-runtime-call
Browse files Browse the repository at this point in the history
* feat: add runtime call

* feat: fetch outstandingDebt with runtime call
Fixes #147

* feat: initialise loan props

* chore: loan logging

* chore: formatting
  • Loading branch information
filo87 authored Feb 2, 2024
1 parent 4192462 commit 4e4b3d5
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 76 deletions.
28 changes: 25 additions & 3 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type Pool @entity {
type: String! @index
isActive: Boolean! @index


createdAt: Date
createdAtBlockNumber: Int

Expand Down Expand Up @@ -309,7 +308,6 @@ type Loan @entity {
probabilityOfDefault: BigInt
lossGivenDefault: BigInt
discountRate: BigInt
maturityDate: Date

interestRatePerSec: BigInt

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -392,4 +414,4 @@ type CurrencyBalance @entity {

type Blockchain @entity {
id: ID! #EVM chainId
}
}
46 changes: 46 additions & 0 deletions src/chaintypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PalletLoansEntitiesLoansActiveLoan>',
},
},
version: 1,
},
],
},
rpc: {
pools: {
trancheTokenPrices: {
Expand Down
19 changes: 16 additions & 3 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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]>
Expand Down Expand Up @@ -339,7 +346,7 @@ export type InvestOrdersCollectedEvent = ITuple<
who: AccountId32,
processedOrders: Vec<u64>,
collection: InvestCollection,
outcome: Enum
outcome: Enum,
]
>
export type RedeemOrdersCollectedEvent = ITuple<
Expand All @@ -364,3 +371,9 @@ export type ExtendedRpc = typeof api.rpc & {
trancheTokenPrices: PromiseRpcResult<AugmentedRpc<(poolId: number | string) => Observable<Vec<u128>>>>
}
}

export type ExtendedCall = typeof api.call & {
loansApi: {
portfolio: AugmentedCall<'promise', (poolId: string) => Observable<Vec<ITuple<[u64, LoanInfoActivePortfolio]>>>>
}
}
17 changes: 8 additions & 9 deletions src/mappings/handlers/blockHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -45,16 +46,14 @@ async function _handleBlock(block: SubstrateBlock): Promise<void> {
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()
Expand Down
20 changes: 0 additions & 20 deletions src/mappings/services/loanService.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import { bnToBn, nToBigInt } from '@polkadot/util'
import { RAY, WAD } from '../../config'

import { LoanService } from './loanService'

const poolId = '1111111111'
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,
Expand Down Expand Up @@ -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)
})
})
31 changes: 16 additions & 15 deletions src/mappings/services/loanService.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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<Vec<InterestAccrualRateDetails>>()
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()}`)
}

Expand Down
70 changes: 44 additions & 26 deletions src/mappings/services/poolService.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -180,25 +179,30 @@ export class PoolService extends Pool {
return tranches.reduce<PoolTranches>((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<Vec<ITuple<[u64, LoanInfoActive]>>>(this.id)
const activeLoanData = loanDetails.reduce<ActiveLoanData>(
(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<ActiveLoanData> {
const apiCall = api.call as ExtendedCall
const portfolioData = await apiCall.loansApi.portfolio(this.id)
return portfolioData.reduce<ActiveLoanData>((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() {
Expand All @@ -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 {
Expand Down

0 comments on commit 4e4b3d5

Please sign in to comment.