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) + } +}