Skip to content

Commit

Permalink
feat: track future cashflows per asset (#219)
Browse files Browse the repository at this point in the history
* feat: track future cashflows per asset
Fixes #196

* feat: track future cashflows
Closes #196
Closes #220

* chore: record cashflows for debt transfers

* fix: runtime typing
  • Loading branch information
filo87 committed Jul 2, 2024
1 parent 8a13a97 commit 99ea318
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 87 deletions.
9 changes: 9 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ type AssetTransaction @entity {
realizedProfitFifo: BigInt
}

type AssetCashflow @entity {
id: ID! # pool id - asset id - cf timestamp
asset: Asset! @index

timestamp: Date!
principal: BigInt!
interest: BigInt!
}

type OracleTransaction @entity {
id: ID! # extrinsic hash - timestamp - oracle key
timestamp: Date!
Expand Down
120 changes: 59 additions & 61 deletions src/chaintypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { OverrideBundleDefinition } from '@polkadot/types/types'

/* eslint-disable sort-keys */
import type { DefinitionsCallEntry, OverrideBundleDefinition } from '@polkadot/types/types'

const latestTypes = {
ActiveLoanInfoV2: {
Expand Down Expand Up @@ -36,6 +34,61 @@ const latestTypes = {
bucket: 'CfgTraitsFeePoolFeeBucket',
fees: 'Vec<CfgTypesPoolsPoolFee>',
},
CashflowPayment: {
when: 'u64',
principal: 'Balance',
interest: 'Balance',
},
}

const loansRuntimeApiMethodsV1: DefinitionsCallEntry['methods'] = {
portfolio: {
description: 'Get active pool loan',
params: [
{
name: 'pool_id',
type: 'u64',
},
],
type: 'Vec<(u64, ActiveLoanInfoV1)>',
},
portfolio_loan: {
description: 'Get active pool loan',
params: [
{
name: 'pool_id',
type: 'u64',
},
{
name: 'loan_id',
type: 'u64',
},
],
type: 'Option<PalletLoansEntitiesLoansActiveLoan>',
},
}

const loansRuntimeApiMethodsV2: DefinitionsCallEntry['methods'] = {
...loansRuntimeApiMethodsV1,
portfolio: { ...loansRuntimeApiMethodsV1.portfolio, type: 'Vec<(u64, ActiveLoanInfoV2)>' },
}

const loansRuntimeApiMethodsV3: DefinitionsCallEntry['methods'] = {
...loansRuntimeApiMethodsV2,
expected_cashflows: {
description: 'Retrieve expected cashflows',
params: [
{
name: 'pool_id',
type: 'u64',
},
{
name: 'loan_id',
type: 'u64',
},
],
type: 'Result<Vec<CashflowPayment<Balance>>, SpRuntimeDispatchError>',
},
}

const definitions: OverrideBundleDefinition = {
Expand All @@ -47,64 +100,9 @@ const definitions: OverrideBundleDefinition = {
],
runtime: {
LoansApi: [
{
methods: {
portfolio: {
description: 'Get active pool loan',
params: [
{
name: 'pool_id',
type: 'u64',
},
],
type: 'Vec<(u64, ActiveLoanInfoV2)>',
},
portfolio_loan: {
description: 'Get active pool loan',
params: [
{
name: 'pool_id',
type: 'u64',
},
{
name: 'loan_id',
type: 'u64',
},
],
type: 'Option<PalletLoansEntitiesLoansActiveLoan>',
},
},
version: 2,
},
{
methods: {
portfolio: {
description: 'Get active pool loan',
params: [
{
name: 'pool_id',
type: 'u64',
},
],
type: 'Vec<(u64, ActiveLoanInfoV1)>',
},
portfolio_loan: {
description: 'Get active pool loan',
params: [
{
name: 'pool_id',
type: 'u64',
},
{
name: 'loan_id',
type: 'u64',
},
],
type: 'Option<PalletLoansEntitiesLoansActiveLoan>',
},
},
version: 1,
},
{ methods: loansRuntimeApiMethodsV3, version: 3 },
{ methods: loansRuntimeApiMethodsV2, version: 2 },
{ methods: loansRuntimeApiMethodsV1, version: 1 },
],
PoolsApi: [
{
Expand Down
50 changes: 31 additions & 19 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//find out types: const a = createType(api.registry, '[u8;32]', 18)
import { AugmentedCall, AugmentedRpc, PromiseRpcResult } from '@polkadot/api/types'
import { Enum, Null, Struct, u128, u32, u64, U8aFixed, Option, Vec, Bytes } from '@polkadot/types'
import { Enum, Null, Struct, u128, u32, u64, U8aFixed, Option, Vec, Bytes, Result } from '@polkadot/types'
import { AccountId32, Perquintill, Balance } from '@polkadot/types/interfaces'
import { ITuple, Observable } from '@polkadot/types/types'

Expand Down Expand Up @@ -265,7 +265,7 @@ export interface LoanPricing extends Enum {
asExternal: {
priceId: OracleKey
maxBorrowAmount: LoanExternalPricingMaxBorrowAmount
notional: u128,
notional: u128
maxPriceVariation: u128
}
}
Expand Down Expand Up @@ -403,32 +403,40 @@ export interface PoolFee extends Struct {
}

export interface OracleKey extends Enum {
readonly isIsin: boolean;
readonly asIsin: U8aFixed;
readonly isConversionRatio: boolean;
readonly asConversionRatio: ITuple<[TokensCurrencyId, TokensCurrencyId]>;
readonly isPoolLoanId: boolean;
readonly asPoolLoanId: ITuple<[u64, u64]>;
readonly type: 'Isin' | 'ConversionRatio' | 'PoolLoanId';
readonly isIsin: boolean
readonly asIsin: U8aFixed
readonly isConversionRatio: boolean
readonly asConversionRatio: ITuple<[TokensCurrencyId, TokensCurrencyId]>
readonly isPoolLoanId: boolean
readonly asPoolLoanId: ITuple<[u64, u64]>
readonly type: 'Isin' | 'ConversionRatio' | 'PoolLoanId'
}

interface DevelopmentRuntimeOriginCaller extends Enum {
readonly isSystem: boolean;
readonly asSystem: unknown//FrameSupportDispatchRawOrigin;
readonly isVoid: boolean;
readonly isCouncil: boolean;
readonly isSystem: boolean
readonly asSystem: unknown //FrameSupportDispatchRawOrigin;
readonly isVoid: boolean
readonly isCouncil: boolean
readonly asCouncil: unknown //PalletCollectiveRawOrigin;
readonly isLiquidityPoolsGateway: boolean;
readonly isLiquidityPoolsGateway: boolean
readonly asLiquidityPoolsGateway: unknown //PalletLiquidityPoolsGatewayOriginGatewayOrigin;
readonly isPolkadotXcm: boolean;
readonly asPolkadotXcm: unknown//PalletXcmOrigin;
readonly isCumulusXcm: boolean;
readonly isPolkadotXcm: boolean
readonly asPolkadotXcm: unknown //PalletXcmOrigin;
readonly isCumulusXcm: boolean
readonly asCumulusXcm: unknown //CumulusPalletXcmOrigin;
readonly isEthereum: boolean;
readonly isEthereum: boolean
readonly asEthereum: unknown //PalletEthereumRawOrigin;
readonly type: 'System' | 'Void' | 'Council' | 'LiquidityPoolsGateway' | 'PolkadotXcm' | 'CumulusXcm' | 'Ethereum';
readonly type: 'System' | 'Void' | 'Council' | 'LiquidityPoolsGateway' | 'PolkadotXcm' | 'CumulusXcm' | 'Ethereum'
}

export interface CashflowPayment extends Struct {
when: u64
principal: Balance
interest: Balance
}

export interface DispatchError extends Enum {}

export type LoanAsset = ITuple<[collectionId: u64, itemId: u128]>
export type LoanCreatedEvent = ITuple<[poolId: u64, loanId: u64, loanInfo: LoanInfoCreated]>
export type LoanClosedEvent = ITuple<[poolId: u64, loanId: u64, collateralInfo: LoanAsset]>
Expand Down Expand Up @@ -492,6 +500,10 @@ export type ExtendedRpc = typeof api.rpc & {
export type ExtendedCall = typeof api.call & {
loansApi: {
portfolio: AugmentedCall<'promise', (poolId: string) => Observable<Vec<ITuple<[u64, LoanInfoActivePortfolio]>>>>
expectedCashflows: AugmentedCall<
'promise',
(poolId: string, loanId: string) => Observable<Result<Vec<CashflowPayment>, DispatchError>>
>
}
poolsApi: {
nav: AugmentedCall<'promise', (poolId: string) => Observable<Option<PoolNav>>>
Expand Down
36 changes: 29 additions & 7 deletions src/mappings/handlers/loansHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AssetType, AssetValuationMethod } from '../../types'
import { bnToBn, nToBigInt } from '@polkadot/util'
import { WAD } from '../../config'
import { AssetPositionService } from '../services/assetPositionService'
import { AssetCashflowService } from '../services/assetCashflowService'

export const handleLoanCreated = errorHandler(_handleLoanCreated)
async function _handleLoanCreated(event: SubstrateEvent<LoanCreatedEvent>) {
Expand Down Expand Up @@ -96,6 +97,9 @@ async function _handleLoanCreated(event: SubstrateEvent<LoanCreatedEvent>) {
// Update pool info
await pool.increaseNumberOfAssets()
await pool.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(asset.id)
}

export const handleLoanBorrowed = errorHandler(_handleLoanBorrowed)
Expand Down Expand Up @@ -168,6 +172,9 @@ async function _handleLoanBorrowed(event: SubstrateEvent<LoanBorrowedEvent>): Pr
await epoch.save()
}
await asset.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(asset.id)
}

export const handleLoanRepaid = errorHandler(_handleLoanRepaid)
Expand Down Expand Up @@ -245,22 +252,28 @@ async function _handleLoanRepaid(event: SubstrateEvent<LoanRepaidEvent>) {
}

await asset.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(asset.id)
}

export const handleLoanWrittenOff = errorHandler(_handleLoanWrittenOff)
async function _handleLoanWrittenOff(event: SubstrateEvent<LoanWrittenOffEvent>) {
const [poolId, loanId, status] = event.event.data
logger.info(`Loan writtenoff event for pool: ${poolId.toString()} loanId: ${loanId.toString()}`)
const { percentage, penalty } = status
const loan = await AssetService.getById(poolId.toString(), loanId.toString())
await loan.writeOff(percentage.toBigInt(), penalty.toBigInt())
await loan.save()
const asset = await AssetService.getById(poolId.toString(), loanId.toString())
await asset.writeOff(percentage.toBigInt(), penalty.toBigInt())
await asset.save()

const pool = await PoolService.getById(poolId.toString())
if (pool === undefined) throw missingPool

await pool.increaseWriteOff(loan.writtenOffAmountByPeriod)
await pool.increaseWriteOff(asset.writtenOffAmountByPeriod)
await pool.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(asset.id)
}

export const handleLoanClosed = errorHandler(_handleLoanClosed)
Expand All @@ -273,9 +286,9 @@ async function _handleLoanClosed(event: SubstrateEvent<LoanClosedEvent>) {

const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex())

const loan = await AssetService.getById(poolId.toString(), loanId.toString())
await loan.close()
await loan.save()
const asset = await AssetService.getById(poolId.toString(), loanId.toString())
await asset.close()
await asset.save()

const epoch = await EpochService.getById(pool.id, pool.currentEpoch)
if (!epoch) throw new Error('Epoch not found!')
Expand All @@ -289,6 +302,9 @@ async function _handleLoanClosed(event: SubstrateEvent<LoanClosedEvent>) {
timestamp: event.block.timestamp,
})
await at.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(asset.id)
}

export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred)
Expand Down Expand Up @@ -375,6 +391,9 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent<LoanDebtTransfer
realizedProfitFifo,
})
await principalRepayment.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(principalRepayment.assetId)
}

if (fromAsset.isOffchainCash() && toAsset.isNonCash()) {
Expand Down Expand Up @@ -415,6 +434,9 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent<LoanDebtTransfer
fromAssetId: fromLoanId.toString(10),
})
await purchaseTransaction.save()

// Record cashflows
await AssetCashflowService.recordAssetCashflows(purchaseTransaction.assetId)
}
}

Expand Down
37 changes: 37 additions & 0 deletions src/mappings/services/assetCashflowService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ExtendedCall } from '../../helpers/types'
import { AssetCashflow } from '../../types/models/AssetCashflow'

export class AssetCashflowService extends AssetCashflow {
static init(assetId: string, timestamp: Date, principal: bigint, interest: bigint) {
const id = `${assetId}-${timestamp.valueOf()}`
logger.info(`Initialising new AssetCashflow with Id ${id}`)
return new this(id, assetId, timestamp, principal, interest)
}

static async recordAssetCashflows(_assetId: string) {
const specVersion = api.runtimeVersion.specVersion.toNumber()
if (specVersion < 1103) return
const [poolId, assetId] = _assetId.split('-')
logger.info(`Recording AssetCashflows for Asset ${_assetId}`)
const apiCall = api.call as ExtendedCall
logger.info(`Calling runtime API loansApi.expectedCashflows(${poolId}, ${assetId})`)
const response = await apiCall.loansApi.expectedCashflows(poolId, assetId)
logger.info(JSON.stringify(response))
if(!response.isOk) return
await this.clearAssetCashflows(_assetId)
const saves = response.asOk.map((cf) => {
const { when, principal, interest } = cf
const timestamp = new Date(when.toNumber() * 1000)
const cashflow = this.init(_assetId, timestamp, principal.toBigInt(), interest.toBigInt())
return cashflow.save()
})
return Promise.all(saves)
}

static async clearAssetCashflows(assetId: string) {
logger.info(`Clearing AssetCashflows for asset: ${assetId}`)
const cashflows = await this.getByAssetId(assetId)
const deletes = cashflows.map((cf) => this.remove(cf.id))
return Promise.all(deletes)
}
}

0 comments on commit 99ea318

Please sign in to comment.