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..2b7a1fd2 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: { @@ -36,6 +34,61 @@ const latestTypes = { bucket: 'CfgTraitsFeePoolFeeBucket', fees: 'Vec', }, + 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', + }, +} + +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>, SpRuntimeDispatchError>', + }, } const definitions: OverrideBundleDefinition = { @@ -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', - }, - }, - 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/helpers/types.ts b/src/helpers/types.ts index b8aad749..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' @@ -265,7 +265,7 @@ export interface LoanPricing extends Enum { asExternal: { priceId: OracleKey maxBorrowAmount: LoanExternalPricingMaxBorrowAmount - notional: u128, + notional: u128 maxPriceVariation: u128 } } @@ -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]> @@ -492,6 +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, DispatchError>> + > } poolsApi: { nav: AugmentedCall<'promise', (poolId: string) => Observable>> diff --git a/src/mappings/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index 24076059..412de84a 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(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(asset.id) } export const handleLoanRepaid = errorHandler(_handleLoanRepaid) @@ -245,6 +252,9 @@ async function _handleLoanRepaid(event: SubstrateEvent) { } await asset.save() + + // Record cashflows + await AssetCashflowService.recordAssetCashflows(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(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(asset.id) } export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred) @@ -375,6 +391,9 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent { + 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) + } +}