diff --git a/.envrc b/.envrc index dfa3c81b..afb0e657 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,4 @@ -export COMPOSE_PROFILES=subql +export COMPOSE_PROFILES=subql-cfg export DB_USER=postgres export DB_PASS=postgres export DB_DATABASE=postgres diff --git a/chains-cfg/_root.yaml b/chains-cfg/_root.yaml index ef5c8a40..1d5ddf75 100644 --- a/chains-cfg/_root.yaml +++ b/chains-cfg/_root.yaml @@ -21,7 +21,8 @@ dataSources: - handler: handleBlock kind: substrate/BlockHandler filter: - timestamp: '1 0,12 * * *' + modulo: 300 + #timestamp: '1 0,12 * * *' # - handler: logEvents # kind: substrate/EventHandler - handler: handlePoolCreated @@ -109,11 +110,6 @@ dataSources: filter: module: ormlTokens method: Transfer - - handler: handleTokenEndowed - kind: substrate/EventHandler - filter: - module: ormlTokens - method: Endowed - handler: handleTokenDeposited kind: substrate/EventHandler filter: diff --git a/chains-cfg/centrifuge.yaml b/chains-cfg/centrifuge.yaml index b76857d3..f705b96d 100644 --- a/chains-cfg/centrifuge.yaml +++ b/chains-cfg/centrifuge.yaml @@ -5,6 +5,8 @@ network: chainId: '0xb3db41421702df9a7fcac62b53ffeac85f7853cc4e689e0b93aeb3db18c09d82' chaintypes: file: ./dist/chaintypes.js + bypassBlocks: + - "3858150-4216110" dataSources: - kind: substrate/Runtime startBlock: 3858140 # block first pool was created at diff --git a/schema.graphql b/schema.graphql index cf001290..d804a3fa 100644 --- a/schema.graphql +++ b/schema.graphql @@ -11,7 +11,6 @@ type Pool @entity { type: String! @index isActive: Boolean! @index - createdAt: Date createdAtBlockNumber: Int @@ -37,6 +36,10 @@ type Pool @entity { # Aggregated transaction data over the last period sumBorrowedAmountByPeriod: BigInt sumRepaidAmountByPeriod: BigInt + sumPrincipalRepaidAmountByPeriod: BigInt + sumInterestRepaidAmountByPeriod: BigInt + sumUnscheduledRepaidAmountByPeriod: BigInt + sumInvestedAmountByPeriod: BigInt sumRedeemedAmountByPeriod: BigInt sumNumberOfLoansByPeriod: BigInt @@ -48,6 +51,9 @@ type Pool @entity { # Cumulated transaction data since pool creation sumBorrowedAmount: BigInt sumRepaidAmount: BigInt + sumPrincipalRepaidAmount: BigInt + sumInterestRepaidAmount: BigInt + sumUnscheduledRepaidAmount: BigInt sumNumberOfLoans: BigInt tranches: [Tranche] @derivedFrom(field: "pool") @@ -73,15 +79,24 @@ type PoolSnapshot @entity { # Aggregated transaction data over the last period sumBorrowedAmountByPeriod: BigInt sumRepaidAmountByPeriod: BigInt + sumPrincipalRepaidAmountByPeriod: BigInt + sumInterestRepaidAmountByPeriod: BigInt + sumUnscheduledRepaidAmountByPeriod: BigInt + sumInvestedAmountByPeriod: BigInt sumRedeemedAmountByPeriod: BigInt sumNumberOfLoansByPeriod: BigInt sumNumberOfActiveLoans: BigInt + sumDebtOverdue: BigInt + sumDebtWrittenOffByPeriod: BigInt # Cumulated transaction data since pool creation sumBorrowedAmount: BigInt sumRepaidAmount: BigInt + sumPrincipalRepaidAmount: BigInt + sumInterestRepaidAmount: BigInt + sumUnscheduledRepaidAmount: BigInt sumNumberOfLoans: BigInt } @@ -309,7 +324,6 @@ type Loan @entity { probabilityOfDefault: BigInt lossGivenDefault: BigInt discountRate: BigInt - maturityDate: Date interestRatePerSec: BigInt @@ -320,7 +334,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 +365,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 @@ -371,6 +409,8 @@ type Currency @entity { id: ID! # chainId - currencySpec - [currencySpec] chain: Blockchain! decimals: Int! + name: String + symbol: String tokenAddress: String escrowAddress: String @@ -390,4 +430,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/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index fd95088e..04139b00 100644 --- a/src/mappings/handlers/loansHandlers.ts +++ b/src/mappings/handlers/loansHandlers.ts @@ -1,4 +1,5 @@ import { SubstrateEvent } from '@subql/types' +import { nToBigInt } from '@polkadot/util' import { LoanBorrowedEvent, LoanClosedEvent, @@ -21,7 +22,7 @@ async function _handleLoanCreated(event: SubstrateEvent) { logger.info(`Loan created event for pool: ${poolId.toString()} loan: ${loanId.toString()}`) const pool = await PoolService.getById(poolId.toString()) - if (pool === undefined) throw missingPool + if (!pool) throw missingPool const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) @@ -82,13 +83,13 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr const [poolId, loanId, borrowAmount] = event.event.data const pool = await PoolService.getById(poolId.toString()) - if (pool === undefined) throw missingPool + if (!pool) throw missingPool const amount = borrowAmount.isInternal - ? borrowAmount.asInternal.toString() - : borrowAmount.asExternal.quantity.mul(borrowAmount.asExternal.settlementPrice).div(WAD).toString() + ? borrowAmount.asInternal.toBigInt() + : nToBigInt(borrowAmount.asExternal.quantity.toBn().mul(borrowAmount.asExternal.settlementPrice.toBn()).div(WAD)) - if (amount == '0') return + if (amount === BigInt(0)) return logger.info(`Loan borrowed event for pool: ${poolId.toString()} amount: ${amount.toString()}`) @@ -97,7 +98,7 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr // Update loan amount const loan = await LoanService.getById(poolId.toString(), loanId.toString()) await loan.activate() - await loan.borrow(BigInt(amount)) + await loan.borrow(amount) await loan.updateItemMetadata() await loan.save() @@ -108,15 +109,15 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr epochNumber: pool.currentEpoch, hash: event.extrinsic.extrinsic.hash.toString(), timestamp: event.block.timestamp, - amount: BigInt(amount), - principalAmount: BigInt(amount), - quantity: borrowAmount.isExternal ? BigInt(borrowAmount.asExternal.quantity.toString()) : null, - settlementPrice: borrowAmount.isExternal ? BigInt(borrowAmount.asExternal.settlementPrice.toString()) : null, + amount: amount, + principalAmount: amount, + quantity: borrowAmount.isExternal ? borrowAmount.asExternal.quantity.toBigInt() : null, + settlementPrice: borrowAmount.isExternal ? borrowAmount.asExternal.settlementPrice.toBigInt() : null, }) await bt.save() // Update pool info - await pool.increaseBorrowings(BigInt(amount)) + await pool.increaseBorrowings(amount) await pool.save() // Update epoch info @@ -131,21 +132,21 @@ async function _handleLoanRepaid(event: SubstrateEvent) { const [poolId, loanId, { principal, interest, unscheduled }] = event.event.data const pool = await PoolService.getById(poolId.toString()) - if (pool === undefined) throw missingPool + if (!pool) throw missingPool const principalAmount = principal.isInternal - ? principal.asInternal - : principal.asExternal.quantity.mul(principal.asExternal.settlementPrice).div(WAD) - const amount = principalAmount.add(interest).add(unscheduled).toString() + ? principal.asInternal.toBigInt() + : nToBigInt(principal.asExternal.quantity.toBn().mul(principal.asExternal.settlementPrice.toBn()).div(WAD)) + const amount = principalAmount + interest.toBigInt() + unscheduled.toBigInt() - if (amount == '0') return + if (amount === BigInt(0)) return logger.info(`Loan repaid event for pool: ${poolId.toString()} amount: ${amount.toString()}`) const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const loan = await LoanService.getById(poolId.toString(), loanId.toString()) - await loan.repay(BigInt(amount)) + await loan.repay(amount) await loan.updateItemMetadata() await loan.save() @@ -156,23 +157,23 @@ async function _handleLoanRepaid(event: SubstrateEvent) { epochNumber: pool.currentEpoch, hash: event.extrinsic.extrinsic.hash.toString(), timestamp: event.block.timestamp, - amount: BigInt(amount), - principalAmount: BigInt(principalAmount.toString()), - interestAmount: BigInt(interest.toString()), - unscheduledAmount: BigInt(unscheduled.toString()), - quantity: principal.isExternal ? BigInt(principal.asExternal.quantity.toString()) : null, - settlementPrice: principal.isExternal ? BigInt(principal.asExternal.settlementPrice.toString()) : null, + amount: amount, + principalAmount: principalAmount, + interestAmount: interest.toBigInt(), + unscheduledAmount: unscheduled.toBigInt(), + quantity: principal.isExternal ? principal.asExternal.quantity.toBigInt() : null, + settlementPrice: principal.isExternal ? principal.asExternal.settlementPrice.toBigInt() : null, }) await bt.save() // Update pool info - await pool.increaseRepayments(BigInt(amount)) + await pool.increaseRepayments(principalAmount, interest.toBigInt(), unscheduled.toBigInt()) await pool.save() // Update epoch info const epoch = await EpochService.getById(pool.id, pool.currentEpoch) - if (epoch === undefined) throw new Error('Epoch not found!') - await epoch.increaseRepayments(BigInt(amount)) + if (!epoch) throw new Error('Epoch not found!') + await epoch.increaseRepayments(amount) await epoch.save() } @@ -224,9 +225,9 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent = { poolId: poolId.toString(), - //loanId: loanId.toString(), address: account.id, epochNumber: pool.currentEpoch, hash: event.extrinsic.extrinsic.hash.toString(), diff --git a/src/mappings/handlers/ormlTokensHandlers.ts b/src/mappings/handlers/ormlTokensHandlers.ts index a1fb18b4..a4492379 100644 --- a/src/mappings/handlers/ormlTokensHandlers.ts +++ b/src/mappings/handlers/ormlTokensHandlers.ts @@ -91,26 +91,6 @@ async function _handleTokenTransfer(event: SubstrateEvent): } } -export const handleTokenEndowed = errorHandler(_handleTokenEndowed) -async function _handleTokenEndowed(event: SubstrateEvent): Promise { - const [_currency, address, amount] = event.event.data - if (_currency.isTranche) return - logger.info( - `Currency endowment in ${_currency.toString()} for: ${address.toHex()} amount: ${amount.toString()} ` + - `at block ${event.block.block.header.number.toString()}` - ) - const blockchain = await BlockchainService.getOrInit() - const currency = await CurrencyService.getOrInit( - blockchain.id, - _currency.type, - ...currencyFormatters[_currency.type](_currency.value) - ) - const toAccount = await AccountService.getOrInit(address.toHex()) - const toCurrencyBalance = await CurrencyBalanceService.getOrInit(toAccount.id, currency.id) - await toCurrencyBalance.credit(amount.toBigInt()) - await toCurrencyBalance.save() -} - export const handleTokenDeposited = errorHandler(_handleTokenDeposited) async function _handleTokenDeposited(event: SubstrateEvent): Promise { const [_currency, address, amount] = event.event.data diff --git a/src/mappings/services/currencyService.ts b/src/mappings/services/currencyService.ts index d269bab3..d3f53a14 100644 --- a/src/mappings/services/currencyService.ts +++ b/src/mappings/services/currencyService.ts @@ -5,10 +5,12 @@ import { WAD_DIGITS } from '../../config' import type { TokensCurrencyId } from '../../helpers/types' export class CurrencyService extends Currency { - static init(chainId: string, currencyId: string, decimals: number) { + static init(chainId: string, currencyId: string, decimals: number, symbol?: string, name?: string) { const id = `${chainId}-${currencyId}` logger.info(`Initialising new currency ${id} with ${decimals} decimals`) const currency = new this(id, chainId, decimals) + currency.symbol = symbol + currency.name = name return currency } @@ -20,7 +22,9 @@ export class CurrencyService extends Currency { const enumPayload = formatEnumPayload(currencyType, ...currencyValue) const assetMetadata = (await api.query.ormlAssetRegistry.metadata(enumPayload)) as Option const decimals = assetMetadata.isSome ? assetMetadata.unwrap().decimals.toNumber() : WAD_DIGITS - currency = this.init(chainId, currencyId, decimals) + const symbol = assetMetadata.isSome ? assetMetadata.unwrap().symbol.toUtf8() : undefined + const name = assetMetadata.isSome ? assetMetadata.unwrap().name.toUtf8() : undefined + currency = this.init(chainId, currencyId, decimals, symbol, name) await currency.save() } return currency as CurrencyService 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..99727bbe 100644 --- a/src/mappings/services/loanService.ts +++ b/src/mappings/services/loanService.ts @@ -1,17 +1,26 @@ -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.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) + loan.borrowedAmountByPeriod = BigInt(0) loan.repaidAmountByPeriod = BigInt(0) @@ -47,6 +56,7 @@ export class LoanService extends Loan { } public updateLoanSpecs(decodedLoanSpecs: LoanSpecs) { + logger.info(`Updating loan specs for ${this.id}`) Object.assign(this, decodedLoanSpecs) } @@ -62,14 +72,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.test.ts b/src/mappings/services/poolService.test.ts index d776ae00..6f55caf5 100644 --- a/src/mappings/services/poolService.test.ts +++ b/src/mappings/services/poolService.test.ts @@ -87,13 +87,13 @@ describe('Given a new pool, when initialised', () => { describe('Given an existing pool,', () => { test('when the nav is updated, then the value is fetched and set correctly', async () => { await pool.updatePortfolioValuation() - expect(api.query.loans.portfolioValuation).toBeCalled() + expect(api.query.loans.portfolioValuation).toHaveBeenCalled() expect(pool.portfolioValuation).toBe(BigInt(100000000000000)) }) test('when the pool state is updated, then the values are fetched and set correctly', async () => { await pool.updateState() - expect(api.query.poolSystem.pool).toBeCalledWith(poolId) + expect(api.query.poolSystem.pool).toHaveBeenCalledWith(poolId) expect(pool).toMatchObject({ totalReserve: BigInt(91000000000000), availableReserve: BigInt(92000000000000), @@ -110,10 +110,16 @@ describe('Given an existing pool,', () => { }) test('when total repaid are registered, then values are incremented correctly', async () => { - await pool.increaseRepayments(BigInt('17500000000000000')) + await pool.increaseRepayments(BigInt('17500000000000000'), BigInt('17500000000000000'), BigInt('17500000000000000')) expect(pool).toMatchObject({ - sumRepaidAmountByPeriod: BigInt('17500000000000000'), - sumRepaidAmount: BigInt('17500000000000000'), + sumRepaidAmountByPeriod: BigInt('17500000000000000') + BigInt('17500000000000000') + BigInt('17500000000000000'), + sumRepaidAmount: BigInt('17500000000000000') + BigInt('17500000000000000') + BigInt('17500000000000000'), + sumPrincipalRepaidAmountByPeriod: BigInt('17500000000000000'), + sumPrincipalRepaidAmount: BigInt('17500000000000000'), + sumInterestRepaidAmountByPeriod: BigInt('17500000000000000'), + sumInterestRepaidAmount: BigInt('17500000000000000'), + sumUnscheduledRepaidAmountByPeriod: BigInt('17500000000000000'), + sumUnscheduledRepaidAmount: BigInt('17500000000000000'), }) }) diff --git a/src/mappings/services/poolService.ts b/src/mappings/services/poolService.ts index 09f0b8f8..c827761a 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 } @@ -52,12 +51,18 @@ export class PoolService extends Pool { this.sumBorrowedAmountByPeriod = BigInt(0) this.sumRepaidAmountByPeriod = BigInt(0) + this.sumPrincipalRepaidAmountByPeriod = BigInt(0) + this.sumInterestRepaidAmountByPeriod = BigInt(0) + this.sumUnscheduledRepaidAmountByPeriod = BigInt(0) this.sumInvestedAmountByPeriod = BigInt(0) this.sumRedeemedAmountByPeriod = BigInt(0) this.sumNumberOfLoansByPeriod = BigInt(0) this.sumBorrowedAmount = BigInt(0) this.sumRepaidAmount = BigInt(0) + this.sumPrincipalRepaidAmount = BigInt(0) + this.sumInterestRepaidAmount = BigInt(0) + this.sumUnscheduledRepaidAmount = BigInt(0) this.sumNumberOfLoans = BigInt(0) this.currencyId = currencyId @@ -128,9 +133,19 @@ export class PoolService extends Pool { this.sumBorrowedAmount += borrowedAmount } - public increaseRepayments(repaidAmount: bigint) { - this.sumRepaidAmountByPeriod += repaidAmount - this.sumRepaidAmount += repaidAmount + public increaseRepayments( + principalRepaidAmount: bigint, + interestRepaidAmount: bigint, + unscheduledRepaidAmount: bigint + ) { + this.sumRepaidAmountByPeriod += principalRepaidAmount + interestRepaidAmount + unscheduledRepaidAmount + this.sumRepaidAmount += principalRepaidAmount + interestRepaidAmount + unscheduledRepaidAmount + this.sumPrincipalRepaidAmountByPeriod += principalRepaidAmount + this.sumPrincipalRepaidAmount += principalRepaidAmount + this.sumInterestRepaidAmountByPeriod += interestRepaidAmount + this.sumInterestRepaidAmount += interestRepaidAmount + this.sumUnscheduledRepaidAmountByPeriod += unscheduledRepaidAmount + this.sumUnscheduledRepaidAmount += unscheduledRepaidAmount } public increaseInvestments(currencyAmount: bigint) { @@ -180,25 +195,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 +235,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 {