From 7a70b5587bf8ec1084ad6cfd6e3f4f056ebc0e3e Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Fri, 28 Jun 2024 17:03:17 +0200 Subject: [PATCH 1/4] feat: track future cashflows per asset Fixes #196 --- schema.graphql | 9 ++++ src/chaintypes.ts | 19 ++++++++ src/helpers/types.ts | 43 +++++++++++-------- src/mappings/handlers/loansHandlers.ts | 30 ++++++++++--- src/mappings/services/assetCashflowService.ts | 29 +++++++++++++ 5 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 src/mappings/services/assetCashflowService.ts diff --git a/schema.graphql b/schema.graphql index 499698a0..11e467b4 100644 --- a/schema.graphql +++ b/schema.graphql @@ -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! diff --git a/src/chaintypes.ts b/src/chaintypes.ts index 53305f1f..8d1addda 100644 --- a/src/chaintypes.ts +++ b/src/chaintypes.ts @@ -36,6 +36,11 @@ const latestTypes = { bucket: 'CfgTraitsFeePoolFeeBucket', fees: 'Vec', }, + CashflowPayment: { + when: 'Seconds', + principal: 'Balance', + interest: 'Balance', + }, } const definitions: OverrideBundleDefinition = { @@ -73,6 +78,20 @@ const definitions: OverrideBundleDefinition = { ], type: 'Option', }, + expected_cashflows: { + description: 'Retrieve expected cashflows', + params: [ + { + name: 'pool_id', + type: 'u64', + }, + { + name: 'loan_id', + type: 'u64', + }, + ], + type: 'Vec', + }, }, version: 2, }, diff --git a/src/helpers/types.ts b/src/helpers/types.ts index b8aad749..a5362c5b 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -265,7 +265,7 @@ export interface LoanPricing extends Enum { asExternal: { priceId: OracleKey maxBorrowAmount: LoanExternalPricingMaxBorrowAmount - notional: u128, + notional: u128 maxPriceVariation: u128 } } @@ -403,30 +403,36 @@ 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 type LoanAsset = ITuple<[collectionId: u64, itemId: u128]> @@ -492,6 +498,7 @@ export type ExtendedRpc = typeof api.rpc & { export type ExtendedCall = typeof api.call & { loansApi: { portfolio: AugmentedCall<'promise', (poolId: string) => Observable>>> + expectedCashflows: AugmentedCall<'promise', (poolId: string, loanId: string) => Observable>> } poolsApi: { nav: AugmentedCall<'promise', (poolId: string) => Observable>> diff --git a/src/mappings/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index 24076059..6bfea15a 100644 --- a/src/mappings/handlers/loansHandlers.ts +++ b/src/mappings/handlers/loansHandlers.ts @@ -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) { @@ -96,6 +97,9 @@ async function _handleLoanCreated(event: SubstrateEvent) { // Update pool info await pool.increaseNumberOfAssets() await pool.save() + + // Record cashflows + await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) } export const handleLoanBorrowed = errorHandler(_handleLoanBorrowed) @@ -168,6 +172,9 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr await epoch.save() } await asset.save() + + // Record cashflows + await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) } export const handleLoanRepaid = errorHandler(_handleLoanRepaid) @@ -245,6 +252,9 @@ async function _handleLoanRepaid(event: SubstrateEvent) { } await asset.save() + + // Record cashflows + await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) } export const handleLoanWrittenOff = errorHandler(_handleLoanWrittenOff) @@ -252,15 +262,18 @@ async function _handleLoanWrittenOff(event: SubstrateEvent) 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(pool.id, asset.id) } export const handleLoanClosed = errorHandler(_handleLoanClosed) @@ -273,9 +286,9 @@ async function _handleLoanClosed(event: SubstrateEvent) { 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!') @@ -289,6 +302,9 @@ async function _handleLoanClosed(event: SubstrateEvent) { timestamp: event.block.timestamp, }) await at.save() + + // Record cashflows + await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) } export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred) diff --git a/src/mappings/services/assetCashflowService.ts b/src/mappings/services/assetCashflowService.ts new file mode 100644 index 00000000..a882bd2a --- /dev/null +++ b/src/mappings/services/assetCashflowService.ts @@ -0,0 +1,29 @@ +import { ExtendedCall } from '../../helpers/types' +import { AssetCashflow } from '../../types/models/AssetCashflow' + +export class AssetCashflowService extends AssetCashflow { + static init(poolId: string, assetId: string, timestamp: Date, principal: bigint, interest: bigint) { + logger.info(`Initialising new AssetCashflow with Id ${chainId}`) + return new this( + `${poolId}-${assetId}-${timestamp}`, + assetId, + timestamp, + principal, + interest + ) + } + + static async recordAssetCashflows(poolId: string, assetId: string) { + const specVersion = api.runtimeVersion.specVersion.toNumber() + if(specVersion < 1103) return + const apiCall = api.call as ExtendedCall + const response = await apiCall.loansApi.expectedCashflows(poolId, assetId) + const saves = response.map( ( cf ) => { + const { when, principal, interest } = cf + const timestamp = new Date(when.toNumber() * 1000) + const cashflow = this.init(poolId, assetId, timestamp, principal.toBigInt(), interest.toBigInt()) + return cashflow.save() + }) + return Promise.all(saves) + } +} From e0f1de9d1d074fec9596b47fe9edab8a628f086f Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Mon, 1 Jul 2024 21:03:54 +0200 Subject: [PATCH 2/4] feat: track future cashflows Closes #196 Closes #220 --- src/chaintypes.ts | 131 ++++++++---------- src/mappings/handlers/loansHandlers.ts | 10 +- src/mappings/services/assetCashflowService.ts | 33 +++-- 3 files changed, 80 insertions(+), 94 deletions(-) diff --git a/src/chaintypes.ts b/src/chaintypes.ts index 8d1addda..699ea38b 100644 --- a/src/chaintypes.ts +++ b/src/chaintypes.ts @@ -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: { @@ -37,12 +35,62 @@ const latestTypes = { fees: 'Vec', }, CashflowPayment: { - when: 'Seconds', + 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', + }, +} + +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: 'Vec', + }, +} + const definitions: OverrideBundleDefinition = { types: [ { @@ -52,78 +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', - }, - expected_cashflows: { - description: 'Retrieve expected cashflows', - params: [ - { - name: 'pool_id', - type: 'u64', - }, - { - name: 'loan_id', - type: 'u64', - }, - ], - type: 'Vec', - }, - }, - 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', - }, - }, - version: 1, - }, + { methods: loansRuntimeApiMethodsV3, version: 3 }, + { methods: loansRuntimeApiMethodsV2, version: 2 }, + { methods: loansRuntimeApiMethodsV1, version: 1 }, ], PoolsApi: [ { diff --git a/src/mappings/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index 6bfea15a..5df623c1 100644 --- a/src/mappings/handlers/loansHandlers.ts +++ b/src/mappings/handlers/loansHandlers.ts @@ -99,7 +99,7 @@ async function _handleLoanCreated(event: SubstrateEvent) { await pool.save() // Record cashflows - await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) + await AssetCashflowService.recordAssetCashflows(asset.id) } export const handleLoanBorrowed = errorHandler(_handleLoanBorrowed) @@ -174,7 +174,7 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr await asset.save() // Record cashflows - await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) + await AssetCashflowService.recordAssetCashflows(asset.id) } export const handleLoanRepaid = errorHandler(_handleLoanRepaid) @@ -254,7 +254,7 @@ async function _handleLoanRepaid(event: SubstrateEvent) { await asset.save() // Record cashflows - await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) + await AssetCashflowService.recordAssetCashflows(asset.id) } export const handleLoanWrittenOff = errorHandler(_handleLoanWrittenOff) @@ -273,7 +273,7 @@ async function _handleLoanWrittenOff(event: SubstrateEvent) await pool.save() // Record cashflows - await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) + await AssetCashflowService.recordAssetCashflows(asset.id) } export const handleLoanClosed = errorHandler(_handleLoanClosed) @@ -304,7 +304,7 @@ async function _handleLoanClosed(event: SubstrateEvent) { await at.save() // Record cashflows - await AssetCashflowService.recordAssetCashflows(pool.id, asset.id) + await AssetCashflowService.recordAssetCashflows(asset.id) } export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred) diff --git a/src/mappings/services/assetCashflowService.ts b/src/mappings/services/assetCashflowService.ts index a882bd2a..8aa52d54 100644 --- a/src/mappings/services/assetCashflowService.ts +++ b/src/mappings/services/assetCashflowService.ts @@ -2,28 +2,35 @@ import { ExtendedCall } from '../../helpers/types' import { AssetCashflow } from '../../types/models/AssetCashflow' export class AssetCashflowService extends AssetCashflow { - static init(poolId: string, assetId: string, timestamp: Date, principal: bigint, interest: bigint) { - logger.info(`Initialising new AssetCashflow with Id ${chainId}`) - return new this( - `${poolId}-${assetId}-${timestamp}`, - assetId, - timestamp, - principal, - interest - ) + 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(poolId: string, assetId: string) { + static async recordAssetCashflows(_assetId: string) { const specVersion = api.runtimeVersion.specVersion.toNumber() - if(specVersion < 1103) return + 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) - const saves = response.map( ( cf ) => { + logger.info(JSON.stringify(response)) + await this.clearAssetCashflows(_assetId) + const saves = response.map((cf) => { const { when, principal, interest } = cf const timestamp = new Date(when.toNumber() * 1000) - const cashflow = this.init(poolId, assetId, timestamp, principal.toBigInt(), interest.toBigInt()) + 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) + } } From 2aee5df1fffbce061b604b9da56993cfc559acf7 Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Mon, 1 Jul 2024 21:22:48 +0200 Subject: [PATCH 3/4] chore: record cashflows for debt transfers --- src/mappings/handlers/loansHandlers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mappings/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index 5df623c1..412de84a 100644 --- a/src/mappings/handlers/loansHandlers.ts +++ b/src/mappings/handlers/loansHandlers.ts @@ -391,6 +391,9 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent Date: Tue, 2 Jul 2024 12:09:15 +0200 Subject: [PATCH 4/4] fix: runtime typing --- src/chaintypes.ts | 2 +- src/helpers/types.ts | 9 +++++++-- src/mappings/services/assetCashflowService.ts | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/chaintypes.ts b/src/chaintypes.ts index 699ea38b..2b7a1fd2 100644 --- a/src/chaintypes.ts +++ b/src/chaintypes.ts @@ -87,7 +87,7 @@ const loansRuntimeApiMethodsV3: DefinitionsCallEntry['methods'] = { type: 'u64', }, ], - type: 'Vec', + type: 'Result>, SpRuntimeDispatchError>', }, } diff --git a/src/helpers/types.ts b/src/helpers/types.ts index a5362c5b..405cc0ca 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -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' @@ -435,6 +435,8 @@ export interface CashflowPayment extends Struct { 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]> @@ -498,7 +500,10 @@ export type ExtendedRpc = typeof api.rpc & { export type ExtendedCall = typeof api.call & { loansApi: { portfolio: AugmentedCall<'promise', (poolId: string) => Observable>>> - expectedCashflows: AugmentedCall<'promise', (poolId: string, loanId: string) => Observable>> + expectedCashflows: AugmentedCall< + 'promise', + (poolId: string, loanId: string) => Observable, DispatchError>> + > } poolsApi: { nav: AugmentedCall<'promise', (poolId: string) => Observable>> diff --git a/src/mappings/services/assetCashflowService.ts b/src/mappings/services/assetCashflowService.ts index 8aa52d54..456ef1f7 100644 --- a/src/mappings/services/assetCashflowService.ts +++ b/src/mappings/services/assetCashflowService.ts @@ -17,8 +17,9 @@ export class AssetCashflowService extends AssetCashflow { 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.map((cf) => { + 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())