Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: track future cashflows per asset #219

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading