From 723e708055d775cfc08839274031fde2458a6384 Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Mon, 11 Nov 2024 20:36:42 +0100 Subject: [PATCH] chore: enable strict mode --- smoke-tests/timekeeper.test.ts | 2 +- smoke-tests/tvl.test.ts | 2 +- src/@types/gobal.d.ts | 2 +- src/helpers/paginatedGetter.ts | 13 +- src/helpers/stateSnapshot.test.ts | 15 +- src/helpers/stateSnapshot.ts | 97 ++++++---- src/helpers/types.ts | 32 +-- src/helpers/validation.ts | 8 + src/index.ts | 4 +- src/mappings/handlers/blockHandlers.ts | 64 ++++-- src/mappings/handlers/ethHandlers.ts | 155 ++++++++------- src/mappings/handlers/evmHandlers.ts | 37 ++-- src/mappings/handlers/investmentsHandlers.ts | 35 +++- src/mappings/handlers/loansHandlers.ts | 145 +++++++++----- src/mappings/handlers/oracleHandlers.ts | 9 +- src/mappings/handlers/ormlTokensHandlers.ts | 24 +-- src/mappings/handlers/poolFeesHandlers.ts | 39 ++-- src/mappings/handlers/poolsHandlers.ts | 84 ++++---- src/mappings/services/assetService.test.ts | 2 +- src/mappings/services/assetService.ts | 73 ++++--- .../services/assetTransactionService.ts | 20 +- src/mappings/services/currencyService.ts | 4 +- src/mappings/services/epochService.ts | 24 ++- .../services/investorTransactionService.ts | 4 +- .../services/oracleTransactionService.ts | 26 +-- .../services/outstandingOrderService.ts | 4 +- src/mappings/services/poolFeeService.ts | 40 +++- src/mappings/services/poolService.test.ts | 2 +- src/mappings/services/poolService.ts | 183 ++++++++++++------ src/mappings/services/trancheService.test.ts | 4 +- src/mappings/services/trancheService.ts | 28 ++- tsconfig.json | 7 +- 32 files changed, 741 insertions(+), 447 deletions(-) create mode 100644 src/helpers/validation.ts diff --git a/smoke-tests/timekeeper.test.ts b/smoke-tests/timekeeper.test.ts index 0712545e..bd76307c 100644 --- a/smoke-tests/timekeeper.test.ts +++ b/smoke-tests/timekeeper.test.ts @@ -12,7 +12,7 @@ describe('SubQl Nodes', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await subql(` { - timekeeper(id: "${chainIds[chain]}") { + timekeeper(id: "${chainIds[chain as keyof typeof chainIds]}") { lastPeriodStart } } diff --git a/smoke-tests/tvl.test.ts b/smoke-tests/tvl.test.ts index 934d9a64..fc289161 100644 --- a/smoke-tests/tvl.test.ts +++ b/smoke-tests/tvl.test.ts @@ -12,6 +12,6 @@ describe('TVL at known intervals', () => { { poolSnapshots(filter: { periodStart: { equalTo: "${sampleDate}" } }) { aggregates { sum { normalizedNAV } } } } `) const { normalizedNAV } = response.data.poolSnapshots.aggregates.sum - expect(normalizedNAV).toBe(knownTVL[sampleDate]) + expect(normalizedNAV).toBe(knownTVL[sampleDate as keyof typeof knownTVL]) }) }) diff --git a/src/@types/gobal.d.ts b/src/@types/gobal.d.ts index 720db9c0..2bfeafcc 100644 --- a/src/@types/gobal.d.ts +++ b/src/@types/gobal.d.ts @@ -1,4 +1,4 @@ export {} declare global { - function getNodeEvmChainId(): Promise + function getNodeEvmChainId(): Promise } diff --git a/src/helpers/paginatedGetter.ts b/src/helpers/paginatedGetter.ts index a5d435ff..cc12bb8b 100644 --- a/src/helpers/paginatedGetter.ts +++ b/src/helpers/paginatedGetter.ts @@ -1,8 +1,9 @@ -import type { Entity, FieldsExpression, GetOptions } from '@subql/types-core' +import type { Entity, FieldsExpression } from '@subql/types-core' +import { EntityClass, EntityProps } from './stateSnapshot' export async function paginatedGetter( entityService: EntityClass, - filter: FieldsExpression[] + filter: FieldsExpression>[] ): Promise { const results: E[] = [] const batch = 100 @@ -15,11 +16,5 @@ export async function paginatedGetter( }) amount = results.push(...entities) } while (entities.length === batch) - return results as E[] -} - -export interface EntityClass { - new (...args): E - getByFields(filter: FieldsExpression[], options?: GetOptions): Promise - create(record): E + return results } diff --git a/src/helpers/stateSnapshot.test.ts b/src/helpers/stateSnapshot.test.ts index 9676432e..6fe06311 100644 --- a/src/helpers/stateSnapshot.test.ts +++ b/src/helpers/stateSnapshot.test.ts @@ -48,15 +48,9 @@ describe('Given a populated pool,', () => { set.mockReset() getByFields.mockReset() getByFields.mockReturnValue([pool]) - await substrateStateSnapshotter( - 'periodId', - periodId, - Pool, - PoolSnapshot, - block, - 'isActive', - true - ) + await substrateStateSnapshotter('periodId', periodId, Pool, PoolSnapshot, block, [ + ['isActive', '=', true], + ]) expect(store.getByFields).toHaveBeenNthCalledWith( 1, 'Pool', @@ -78,8 +72,7 @@ describe('Given a populated pool,', () => { Pool, PoolSnapshot, block, - 'type', - 'ALL', + [['type', '=', 'ALL']], 'poolId' ) expect(store.set).toHaveBeenNthCalledWith( diff --git a/src/helpers/stateSnapshot.ts b/src/helpers/stateSnapshot.ts index c84732ab..82cf1306 100644 --- a/src/helpers/stateSnapshot.ts +++ b/src/helpers/stateSnapshot.ts @@ -1,5 +1,5 @@ -import { EntityClass, paginatedGetter } from './paginatedGetter' -import type { Entity } from '@subql/types-core' +import { paginatedGetter } from './paginatedGetter' +import type { Entity, FieldsExpression, FunctionPropertyNames, GetOptions } from '@subql/types-core' import { EthereumBlock } from '@subql/types-ethereum' import { SubstrateBlock } from '@subql/types' /** @@ -16,43 +16,41 @@ import { SubstrateBlock } from '@subql/types' * @param resetPeriodStates - (optional) reset properties ending in ByPeriod to 0 after snapshot (defaults to true). * @returns A promise resolving when all state manipulations in the DB is completed */ -async function stateSnapshotter( - relationshipField: ForeignKey, +async function stateSnapshotter>( + relationshipField: StringForeignKeys, relationshipId: string, stateModel: EntityClass, snapshotModel: EntityClass, block: { number: number; timestamp: Date }, - filterKey?: keyof T, - filterValue?: T[keyof T], - fkReferenceName?: ForeignKey, + filters?: FieldsExpression>[], + fkReferenceField?: StringForeignKeys, resetPeriodStates = true, - blockchainId: T['blockchainId'] = '0' + blockchainId = '0' ): Promise { const entitySaves: Promise[] = [] logger.info(`Performing snapshots of ${stateModel.prototype._name} for blockchainId ${blockchainId}`) - const filter: Parameters>[1] = [['blockchainId', '=', blockchainId]] - if (filterKey && filterValue) filter.push([filterKey, '=', filterValue]) - const stateEntities = (await paginatedGetter(stateModel, filter)) as SnapshottableEntity[] + const filter = [['blockchainId', '=', blockchainId]] as FieldsExpression>[] + if (filters) filter.push(...filters) + const stateEntities = await paginatedGetter(stateModel, filter) if (stateEntities.length === 0) logger.info(`No ${stateModel.prototype._name} to snapshot!`) for (const stateEntity of stateEntities) { const blockNumber = block.number - const { id, ...copyStateEntity } = stateEntity - logger.info(`Snapshotting ${stateModel.prototype._name}: ${id}`) - const snapshotEntity: SnapshottedEntityProps = snapshotModel.create({ - ...copyStateEntity, - id: `${id}-${blockNumber.toString()}`, + const snapshot: SnapshottedEntity = { + ...stateEntity, + id: `${stateEntity.id}-${blockNumber}`, timestamp: block.timestamp, blockNumber: blockNumber, [relationshipField]: relationshipId, - }) - if (fkReferenceName) snapshotEntity[fkReferenceName] = stateEntity.id - + } + logger.info(`Snapshotting ${stateModel.prototype._name}: ${stateEntity.id}`) + const snapshotEntity = snapshotModel.create(snapshot as U) + if (fkReferenceField) snapshotEntity[fkReferenceField] = stateEntity.id as U[StringForeignKeys] const propNames = Object.getOwnPropertyNames(stateEntity) - const propNamesToReset = propNames.filter((propName) => propName.endsWith('ByPeriod')) as ResettableKey[] + const propNamesToReset = propNames.filter((propName) => propName.endsWith('ByPeriod')) as ResettableKeys[] if (resetPeriodStates) { for (const propName of propNamesToReset) { - logger.debug(`resetting ${stateEntity._name.toLowerCase()}.${propName} to 0`) - stateEntity[propName] = BigInt(0) + logger.debug(`resetting ${stateEntity._name?.toLowerCase()}.${propName} to 0`) + stateEntity[propName] = BigInt(0) as T[ResettableKeys] } entitySaves.push(stateEntity.save()) } @@ -60,68 +58,85 @@ async function stateSnapshotter( - relationshipField: ForeignKey, +export function evmStateSnapshotter>( + relationshipField: StringForeignKeys, relationshipId: string, stateModel: EntityClass, snapshotModel: EntityClass, block: EthereumBlock, - filterKey?: keyof T, - filterValue?: T[keyof T], - fkReferenceName?: ForeignKey, + filters?: FieldsExpression>[], + fkReferenceName?: StringForeignKeys, resetPeriodStates = true ): Promise { const formattedBlock = { number: block.number, timestamp: new Date(Number(block.timestamp) * 1000) } - return stateSnapshotter( + return stateSnapshotter( relationshipField, relationshipId, stateModel, snapshotModel, formattedBlock, - filterKey, - filterValue, + filters, fkReferenceName, resetPeriodStates, '1' ) } -export function substrateStateSnapshotter( - relationshipField: ForeignKey, +export function substrateStateSnapshotter< + T extends SnapshottableEntity, + U extends SnapshottedEntity, +>( + relationshipField: StringForeignKeys, relationshipId: string, stateModel: EntityClass, snapshotModel: EntityClass, block: SubstrateBlock, - filterKey?: keyof T, - filterValue?: T[keyof T], - fkReferenceName?: ForeignKey, + filters?: FieldsExpression>[], + fkReferenceName?: StringForeignKeys, resetPeriodStates = true ): Promise { + if (!block.timestamp) throw new Error('Missing block timestamp') const formattedBlock = { number: block.block.header.number.toNumber(), timestamp: block.timestamp } - return stateSnapshotter( + return stateSnapshotter( relationshipField, relationshipId, stateModel, snapshotModel, formattedBlock, - filterKey, - filterValue, + filters, fkReferenceName, resetPeriodStates, '0' ) } -type ResettableKey = `${string}ByPeriod` -type ForeignKey = `${string}Id` +type ResettableKeyFormat = `${string}ByPeriod` +type ForeignKeyFormat = `${string}Id` +type ResettableKeys = { [K in keyof T]: K extends ResettableKeyFormat ? K : never }[keyof T] +type ForeignKeys = { [K in keyof T]: K extends ForeignKeyFormat ? K : never }[keyof T] +type StringFields = { [K in keyof T]: T[K] extends string | undefined ? K : never }[keyof T] +type StringForeignKeys = NonNullable & StringFields> export interface SnapshottableEntity extends Entity { + save(): Promise blockchainId: string } -export interface SnapshottedEntityProps extends Entity { +export interface SnapshotAdditions { + save(): Promise + id: string blockNumber: number timestamp: Date periodId?: string epochId?: string } + +//type Entries = { [K in keyof T]: [K, T[K]] }[keyof T][] +export type EntityProps = Omit> | '_name'> +export type SnapshottedEntity = SnapshotAdditions & Partial> + +export interface EntityClass { + prototype: { _name: string } + getByFields(filter: FieldsExpression>[], options: GetOptions>): Promise + create(record: EntityProps): T +} diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 07eae3cb..10f3acbd 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -106,20 +106,26 @@ export interface EpochSolution extends Enum { } } +export interface TokensStakingCurrency extends Enum { + readonly isBlockRewards: boolean + readonly type: 'BlockRewards' +} + export interface TokensCurrencyId extends Enum { - isNative: boolean - asNative: null - isTranche: boolean - asTranche: TrancheCurrency | TrancheCurrencyBefore1400 - isAUSD: boolean - asAUSD: null - isForeignAsset: boolean - asForeignAsset: u32 - isStaking: boolean - asStaking: Enum - isLocalAsset: boolean - asLocalAsset: u32 - type: 'Native' | 'Tranche' | 'Ausd' | 'ForeignAsset' | 'Staking' | 'LocalAsset' + readonly isNative: boolean + readonly asNative: unknown + readonly isTranche: boolean + readonly asTranche: TrancheCurrency | TrancheCurrencyBefore1400 + readonly isAusd: boolean + readonly asAusd: unknown + readonly isForeignAsset: boolean + readonly asForeignAsset: u32 + readonly isStaking: boolean + readonly asStaking: TokensStakingCurrency + readonly isLocalAsset: boolean + readonly asLocalAsset: u32 + readonly type: 'Native' | 'Tranche' | 'Ausd' | 'ForeignAsset' | 'Staking' | 'LocalAsset' + get value(): TrancheCurrency & TrancheCurrencyBefore1400 & u32 & TokensStakingCurrency } export interface TrancheSolution extends Struct { diff --git a/src/helpers/validation.ts b/src/helpers/validation.ts new file mode 100644 index 00000000..c27b588f --- /dev/null +++ b/src/helpers/validation.ts @@ -0,0 +1,8 @@ +export function assertPropInitialized( + obj: T, + propertyName: keyof T, + type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' +) { + if (typeof obj[propertyName] !== type) throw new Error(`Property ${propertyName.toString()} not initialized!`) + return obj[propertyName] +} diff --git a/src/index.ts b/src/index.ts index 1949ce2d..761dea75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,10 @@ const isEvmNode = typeof (api as unknown as Provider).getNetwork === 'function' const ethNetworkProm = isEvmNode ? (api as unknown as Provider).getNetwork() : null global.fetch = fetch as unknown as typeof global.fetch -global.atob = atob +global.atob = atob as typeof global.atob global.getNodeEvmChainId = async function () { if (isSubstrateNode) return ((await api.query.evmChainId.chainId()) as u64).toString(10) - if (isEvmNode) return (await ethNetworkProm).chainId.toString(10) + if (isEvmNode) return (await ethNetworkProm)?.chainId.toString(10) } export * from './mappings/handlers/blockHandlers' diff --git a/src/mappings/handlers/blockHandlers.ts b/src/mappings/handlers/blockHandlers.ts index 402022e3..e4d3fd92 100644 --- a/src/mappings/handlers/blockHandlers.ts +++ b/src/mappings/handlers/blockHandlers.ts @@ -28,6 +28,8 @@ const timekeeper = TimekeeperService.init() export const handleBlock = errorHandler(_handleBlock) async function _handleBlock(block: SubstrateBlock): Promise { + if (!block.timestamp) throw new Error('Missing block timestamp') + const blockPeriodStart = getPeriodStart(block.timestamp) const blockNumber = block.block.header.number.toNumber() const newPeriod = (await timekeeper).processBlock(block.timestamp) @@ -54,7 +56,9 @@ async function _handleBlock(block: SubstrateBlock): Promise { const pools = await PoolService.getCfgActivePools() for (const pool of pools) { logger.info(` ## Updating pool ${pool.id} states...`) + if (!pool.currentEpoch) throw new Error('Pool currentEpoch not set') const currentEpoch = await EpochService.getById(pool.id, pool.currentEpoch) + if (!currentEpoch) throw new Error(`Current epoch ${pool.currentEpoch} for pool ${pool.id} not found`) await pool.updateState() await pool.resetDebtOverdue() @@ -63,9 +67,9 @@ async function _handleBlock(block: SubstrateBlock): Promise { const trancheData = await pool.getTranches() const trancheTokenPrices = await pool.getTrancheTokenPrices() for (const tranche of tranches) { - const index = tranche.index - if (trancheTokenPrices) - await tranche.updatePrice(trancheTokenPrices[index].toBigInt(), block.block.header.number.toNumber()) + if (typeof tranche.index !== 'number') throw new Error('Tranche index not set') + if (!trancheTokenPrices) break + await tranche.updatePrice(trancheTokenPrices[tranche.index].toBigInt(), block.block.header.number.toNumber()) await tranche.updateSupply() await tranche.updateDebt(trancheData[tranche.trancheId].debt) await tranche.computeYield('yieldSinceLastPeriod', lastPeriodStart) @@ -83,6 +87,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { limit: 100, })) as TrancheBalanceService[] for (const trancheBalance of trancheBalances) { + if (!tranche.tokenPrice) throw new Error('Tranche token price not set') const unrealizedProfit = await InvestorPositionService.computeUnrealizedProfitAtPrice( trancheBalance.accountId, tranche.id, @@ -98,6 +103,8 @@ async function _handleBlock(block: SubstrateBlock): Promise { pool.resetUnrealizedProfit() for (const loanId in activeLoanData) { const asset = await AssetService.getById(pool.id, loanId) + if (!asset.currentPrice) throw new Error('Asset current price not set') + if (!asset.notional) throw new Error('Asset notional not set') await asset.loadSnapshot(lastPeriodStart) await asset.updateActiveAssetData(activeLoanData[loanId]) await asset.updateUnrealizedProfit( @@ -105,15 +112,32 @@ async function _handleBlock(block: SubstrateBlock): Promise { await AssetPositionService.computeUnrealizedProfitAtPrice(asset.id, asset.notional) ) await asset.save() + + if (typeof asset.interestAccruedByPeriod !== 'bigint') + throw new Error('Asset interest accrued by period not set') await pool.increaseInterestAccrued(asset.interestAccruedByPeriod) - if (asset.isNonCash()) + + if (asset.isNonCash()) { + if (typeof asset.unrealizedProfitAtMarketPrice !== 'bigint') + throw new Error('Asset unrealized profit at market price not set') + if (typeof asset.unrealizedProfitAtNotional !== 'bigint') + throw new Error('Asset unrealized profit at notional not set') + if (typeof asset.unrealizedProfitByPeriod !== 'bigint') + throw new Error('Asset unrealized profit by period not set') pool.increaseUnrealizedProfit( asset.unrealizedProfitAtMarketPrice, asset.unrealizedProfitAtNotional, asset.unrealizedProfitByPeriod ) - if (asset.isBeyondMaturity(block.timestamp)) pool.increaseDebtOverdue(asset.outstandingDebt) - if (asset.isOffchainCash()) pool.increaseOffchainCashValue(asset.presentValue) + } + if (asset.isBeyondMaturity(block.timestamp)) { + if (typeof asset.outstandingDebt !== 'bigint') throw new Error('Asset outstanding debt not set') + pool.increaseDebtOverdue(asset.outstandingDebt) + } + if (asset.isOffchainCash()) { + if (typeof asset.presentValue !== 'bigint') throw new Error('Asset present value not set') + pool.increaseOffchainCashValue(asset.presentValue) + } } await pool.updateNumberOfActiveAssets(BigInt(Object.keys(activeLoanData).length)) @@ -132,6 +156,8 @@ async function _handleBlock(block: SubstrateBlock): Promise { await poolFee.updateAccruals(pending, disbursement) await poolFee.save() + if (typeof poolFee.sumAccruedAmountByPeriod !== 'bigint') + throw new Error('Pool fee sum accrued amount by period not set') await pool.increaseAccruedFees(poolFee.sumAccruedAmountByPeriod) const poolFeeTransaction = PoolFeeTransactionService.accrue({ @@ -153,26 +179,40 @@ async function _handleBlock(block: SubstrateBlock): Promise { logger.info('## Performing snapshots...') //Perform Snapshots and reset accumulators - await substrateStateSnapshotter('periodId', period.id, Pool, PoolSnapshot, block, 'isActive', true, 'poolId') + await substrateStateSnapshotter( + 'periodId', + period.id, + Pool, + PoolSnapshot, + block, + [['isActive', '=', true]], + 'poolId' + ) await substrateStateSnapshotter( 'periodId', period.id, Tranche, TrancheSnapshot, block, - 'isActive', - true, + [['isActive', '=', true]], 'trancheId' ) - await substrateStateSnapshotter('periodId', period.id, Asset, AssetSnapshot, block, 'isActive', true, 'assetId') + await substrateStateSnapshotter( + 'periodId', + period.id, + Asset, + AssetSnapshot, + block, + [['isActive', '=', true]], + 'assetId' + ) await substrateStateSnapshotter( 'periodId', period.id, PoolFee, PoolFeeSnapshot, block, - 'isActive', - true, + [['isActive', '=', true]], 'poolFeeId' ) logger.info('## Snapshotting completed!') diff --git a/src/mappings/handlers/ethHandlers.ts b/src/mappings/handlers/ethHandlers.ts index 8cc868fb..5407e6c4 100644 --- a/src/mappings/handlers/ethHandlers.ts +++ b/src/mappings/handlers/ethHandlers.ts @@ -1,7 +1,7 @@ import { AssetStatus, AssetType, AssetValuationMethod, Pool, PoolSnapshot } from '../../types' import { EthereumBlock } from '@subql/types-ethereum' import { DAIName, DAISymbol, DAIMainnetAddress, multicallAddress, tinlakePools } from '../../config' -import { errorHandler } from '../../helpers/errorHandler' +import { errorHandler, missingPool } from '../../helpers/errorHandler' import { PoolService } from '../services/poolService' import { TrancheService } from '../services/trancheService' import { CurrencyService } from '../services/currencyService' @@ -26,13 +26,6 @@ const timekeeper = TimekeeperService.init() const ALT_1_POOL_ID = '0xf96f18f2c70b57ec864cc0c8b828450b82ff63e3' const ALT_1_END_BLOCK = 20120759 -type PoolMulticall = { - id: string - type: string - call: Multicall3.CallStruct - result: string -} - export const handleEthBlock = errorHandler(_handleEthBlock) async function _handleEthBlock(block: EthereumBlock): Promise { const date = new Date(Number(block.timestamp) * 1000) @@ -50,6 +43,7 @@ async function _handleEthBlock(block: EthereumBlock): Promise { // update pool states const poolUpdateCalls: PoolMulticall[] = [] + for (const tinlakePool of tinlakePools) { if (block.number >= tinlakePool.startBlock) { const pool = await PoolService.getOrSeed(tinlakePool.id, false, false, blockchain.id) @@ -82,7 +76,7 @@ async function _handleEthBlock(block: EthereumBlock): Promise { result: '', }) } - if (latestReserve) { + if (latestReserve && latestReserve.address) { poolUpdateCalls.push({ id: tinlakePool.id, type: 'totalBalance', @@ -99,9 +93,10 @@ async function _handleEthBlock(block: EthereumBlock): Promise { const callResults = await processCalls(poolUpdateCalls) for (const callResult of callResults) { const tinlakePool = tinlakePools.find((p) => p.id === callResult.id) - const latestNavFeed = getLatestContract(tinlakePool?.navFeed, blockNumber) - const latestReserve = getLatestContract(tinlakePool?.reserve, blockNumber) - const pool = await PoolService.getOrSeed(tinlakePool?.id, false, false, blockchain.id) + if (!tinlakePool) throw missingPool + const latestNavFeed = getLatestContract(tinlakePool.navFeed, blockNumber) + const latestReserve = getLatestContract(tinlakePool.reserve, blockNumber) + const pool = await PoolService.getOrSeed(tinlakePool.id, false, false, blockchain.id) // Update pool if (callResult.type === 'currentNAV' && latestNavFeed) { @@ -115,7 +110,7 @@ async function _handleEthBlock(block: EthereumBlock): Promise { pool.netAssetValue = tinlakePool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK ? BigInt(0) - : (pool.portfolioValuation || BigInt(0)) + (pool.totalReserve || BigInt(0)) + : (pool.portfolioValuation ?? BigInt(0)) + (pool.totalReserve ?? BigInt(0)) await pool.updateNormalizedNAV() await pool.save() logger.info(`Updating pool ${tinlakePool?.id} with portfolioValuation: ${pool.portfolioValuation}`) @@ -128,14 +123,14 @@ async function _handleEthBlock(block: EthereumBlock): Promise { .decodeFunctionResult('totalBalance', callResult.result)[0] .toBigInt() pool.totalReserve = totalBalance - pool.netAssetValue = (pool.portfolioValuation || BigInt(0)) + (pool.totalReserve || BigInt(0)) + pool.netAssetValue = (pool.portfolioValuation ?? BigInt(0)) + (pool.totalReserve ?? BigInt(0)) await pool.updateNormalizedNAV() await pool.save() logger.info(`Updating pool ${tinlakePool?.id} with totalReserve: ${pool.totalReserve}`) } // Update loans (only index if fully synced) - if (latestNavFeed && date.toDateString() === new Date().toDateString()) { + if (latestNavFeed && latestNavFeed.address && date.toDateString() === new Date().toDateString()) { await updateLoans( tinlakePool?.id as string, date, @@ -155,8 +150,7 @@ async function _handleEthBlock(block: EthereumBlock): Promise { Pool, PoolSnapshot, block, - 'isActive', - true, + [['isActive', '=', true]], 'poolId' ) //await evmStateSnapshotter('Asset', 'AssetSnapshot', block, 'isActive', true, 'assetId') @@ -277,7 +271,7 @@ async function updateLoans( // update all loans existingLoans = - (await AssetService.getByPoolId(poolId, { limit: 100 }))?.filter((loan) => loan.status !== AssetStatus.CLOSED) || [] + (await AssetService.getByPoolId(poolId, { limit: 100 }))?.filter((loan) => loan.status !== AssetStatus.CLOSED) ?? [] logger.info(`Updating ${existingLoans?.length} existing loans for pool ${poolId}`) const loanDetailsCalls: PoolMulticall[] = [] existingLoans.forEach((loan) => { @@ -312,31 +306,29 @@ async function updateLoans( }) if (loanDetailsCalls.length > 0) { const loanDetailsResponses = await processCalls(loanDetailsCalls) - const loanDetails = {} - for (let i = 0; i < loanDetailsResponses.length; i++) { - if (loanDetailsResponses[i].result) { - if (!loanDetails[loanDetailsResponses[i].id]) { - loanDetails[loanDetailsResponses[i].id] = {} - } - if (loanDetailsResponses[i].type !== 'nftLocked') { - loanDetails[loanDetailsResponses[i].id].nftLocked = ShelfAbi__factory.createInterface().decodeFunctionResult( - 'nftLocked', - loanDetailsResponses[i].result - )[0] - } - if (loanDetailsResponses[i].type === 'debt') { - loanDetails[loanDetailsResponses[i].id].debt = isAlt1AndAfterEndBlock - ? BigInt(0) - : PileAbi__factory.createInterface() - .decodeFunctionResult('debt', loanDetailsResponses[i].result)[0] - .toBigInt() - } - if (loanDetailsResponses[i].type === 'loanRates') { - loanDetails[loanDetailsResponses[i].id].loanRates = PileAbi__factory.createInterface().decodeFunctionResult( - 'loanRates', - loanDetailsResponses[i].result - )[0] - } + const loanDetails: LoanDetails = {} + for (const loanDetailsResponse of loanDetailsResponses) { + const loanId = loanDetailsResponse.id + if (!loanDetailsResponse.result) continue + + if (!loanDetails[loanId]) loanDetails[loanId] = {} + + if (loanDetailsResponse.type !== 'nftLocked') { + loanDetails[loanId].nftLocked = ShelfAbi__factory.createInterface().decodeFunctionResult( + 'nftLocked', + loanDetailsResponse.result + )[0] + } + if (loanDetailsResponse.type === 'debt') { + loanDetails[loanId].debt = isAlt1AndAfterEndBlock + ? BigInt(0) + : PileAbi__factory.createInterface().decodeFunctionResult('debt', loanDetailsResponse.result)[0].toBigInt() + } + if (loanDetailsResponse.type === 'loanRates') { + loanDetails[loanId].loanRates = PileAbi__factory.createInterface().decodeFunctionResult( + 'loanRates', + loanDetailsResponse.result + )[0] } } @@ -346,13 +338,13 @@ async function updateLoans( let sumInterestRatePerSec = BigInt(0) let sumBorrowsCount = BigInt(0) let sumRepaysCount = BigInt(0) - for (let i = 0; i < existingLoans.length; i++) { - const loan = existingLoans[i] + for (const existingLoan of existingLoans) { + const loan = existingLoan const loanIndex = loan.id.split('-')[1] const nftLocked = loanDetails[loanIndex].nftLocked - const prevDebt = loan.outstandingDebt || BigInt(0) + const prevDebt = loan.outstandingDebt ?? BigInt(0) const debt = loanDetails[loanIndex].debt - if (debt > BigInt(0)) { + if (debt && debt > BigInt(0)) { loan.status = AssetStatus.ACTIVE } // if the loan is not locked or the debt is 0 and the loan was active before, close it @@ -362,44 +354,41 @@ async function updateLoans( await loan.save() } loan.outstandingDebt = debt - const currentDebt = loan.outstandingDebt || BigInt(0) + const currentDebt = loan.outstandingDebt ?? BigInt(0) const rateGroup = loanDetails[loanIndex].loanRates const pileContract = PileAbi__factory.connect(pile, api as unknown as Provider) + if (!rateGroup) throw new Error(`Missing rateGroup for loan ${loan.id}`) const rates = await pileContract.rates(rateGroup) loan.interestRatePerSec = rates.ratePerSecond.toBigInt() if (prevDebt > currentDebt) { loan.repaidAmountByPeriod = prevDebt - currentDebt - loan.totalRepaid = (loan.totalRepaid || BigInt(0)) + loan.repaidAmountByPeriod - loan.repaysCount = (loan.repaysCount || BigInt(0)) + BigInt(1) + loan.totalRepaid = (loan.totalRepaid ?? BigInt(0)) + loan.repaidAmountByPeriod + loan.repaysCount = (loan.repaysCount ?? BigInt(0)) + BigInt(1) } if ( prevDebt * (loan.interestRatePerSec / BigInt(10) ** BigInt(27)) * BigInt(86400) < - (loan.outstandingDebt || BigInt(0)) + (loan.outstandingDebt ?? BigInt(0)) ) { - loan.borrowedAmountByPeriod = (loan.outstandingDebt || BigInt(0)) - prevDebt - loan.totalBorrowed = (loan.totalBorrowed || BigInt(0)) + loan.borrowedAmountByPeriod - loan.borrowsCount = (loan.borrowsCount || BigInt(0)) + BigInt(1) + loan.borrowedAmountByPeriod = (loan.outstandingDebt ?? BigInt(0)) - prevDebt + loan.totalBorrowed = (loan.totalBorrowed ?? BigInt(0)) + loan.borrowedAmountByPeriod + loan.borrowsCount = (loan.borrowsCount ?? BigInt(0)) + BigInt(1) } logger.info(`Updating loan ${loan.id} for pool ${poolId}`) await loan.save() - sumDebt += loan.outstandingDebt || BigInt(0) - sumBorrowed += loan.totalBorrowed || BigInt(0) - sumRepaid += loan.totalRepaid || BigInt(0) - sumInterestRatePerSec += (loan.interestRatePerSec || BigInt(0)) * (loan.outstandingDebt || BigInt(0)) - sumBorrowsCount += loan.borrowsCount || BigInt(0) - sumRepaysCount += loan.repaysCount || BigInt(0) + sumDebt += loan.outstandingDebt ?? BigInt(0) + sumBorrowed += loan.totalBorrowed ?? BigInt(0) + sumRepaid += loan.totalRepaid ?? BigInt(0) + sumInterestRatePerSec += (loan.interestRatePerSec ?? BigInt(0)) * (loan.outstandingDebt ?? BigInt(0)) + sumBorrowsCount += loan.borrowsCount ?? BigInt(0) + sumRepaysCount += loan.repaysCount ?? BigInt(0) } pool.sumDebt = sumDebt pool.sumBorrowedAmount = sumBorrowed pool.sumRepaidAmount = sumRepaid - if (sumDebt > BigInt(0)) { - pool.weightedAverageInterestRatePerSec = sumInterestRatePerSec / sumDebt - } else { - pool.weightedAverageInterestRatePerSec = BigInt(0) - } + pool.weightedAverageInterestRatePerSec = sumDebt > BigInt(0) ? sumInterestRatePerSec / sumDebt : BigInt(0) pool.sumBorrowsCount = sumBorrowsCount pool.sumRepaysCount = sumRepaysCount await pool.save() @@ -430,11 +419,9 @@ async function getNewLoans(existingLoans: number[], shelfAddress: string) { return contractLoans.filter((loanIndex) => !existingLoans.includes(loanIndex)) } -function getLatestContract(contractArray, blockNumber) { - return contractArray.reduce( - (prev, current) => - current.startBlock <= blockNumber && current.startBlock > (prev?.startBlock || 0) ? current : prev, - null +function getLatestContract(contractArray: ContractArray[], blockNumber: number) { + return contractArray.reduce((prev, current: ContractArray) => + current.startBlock <= blockNumber && current.startBlock > (prev?.startBlock ?? 0) ? current : prev ) } @@ -452,11 +439,15 @@ async function processCalls(callsArray: PoolMulticall[], chunkSize = 30): Promis const chunk = callChunks[i] const multicall = MulticallAbi__factory.connect(multicallAddress, api as unknown as Provider) // eslint-disable-next-line - let results: any[] = [] + let results: [BigNumber, string[]] & { + blockNumber: BigNumber + returnData: string[] + } try { const calls = chunk.map((call) => call.call) results = await multicall.callStatic.aggregate(calls) - results[1].map((result, j) => (callsArray[i * chunkSize + j].result = result)) + const [_blocknumber, returnData] = results + returnData.forEach((result, j) => (callsArray[i * chunkSize + j].result = result)) } catch (e) { logger.error(`Error fetching chunk ${i}: ${e}`) } @@ -464,3 +455,23 @@ async function processCalls(callsArray: PoolMulticall[], chunkSize = 30): Promis return callsArray } + +interface PoolMulticall { + id: string + type: string + call: Multicall3.CallStruct + result: string +} + +interface LoanDetails { + [loanId: string]: { + nftLocked?: string + debt?: bigint + loanRates?: bigint + } +} + +interface ContractArray { + address: string | null + startBlock: number +} diff --git a/src/mappings/handlers/evmHandlers.ts b/src/mappings/handlers/evmHandlers.ts index 349bcf0d..6ea4f1a6 100644 --- a/src/mappings/handlers/evmHandlers.ts +++ b/src/mappings/handlers/evmHandlers.ts @@ -20,11 +20,13 @@ const _ethApi = api as unknown as Provider export const handleEvmDeployTranche = errorHandler(_handleEvmDeployTranche) async function _handleEvmDeployTranche(event: DeployTrancheLog): Promise { + if (!event.args) throw new Error('Missing event arguments') const [_poolId, _trancheId, tokenAddress] = event.args const poolManagerAddress = event.address await BlockchainService.getOrInit(LOCAL_CHAIN_ID) - const chainId = await getNodeEvmChainId() //(await networkPromise).chainId.toString(10) + const chainId = await getNodeEvmChainId() + if (!chainId) throw new Error('Unable to retrieve chainId') const evmBlockchain = await BlockchainService.getOrInit(chainId) const poolId = _poolId.toString() @@ -44,7 +46,7 @@ async function _handleEvmDeployTranche(event: DeployTrancheLog): Promise { //const escrowAddress = await poolManager.escrow() if (!(poolManagerAddress in escrows)) throw new Error(`Escrow address for PoolManager ${poolManagerAddress} missing in config!`) - const escrowAddress: string = escrows[poolManagerAddress] + const escrowAddress: string = escrows[poolManagerAddress as keyof typeof escrows] await currency.initTrancheDetails(tranche.poolId, tranche.trancheId, tokenAddress, escrowAddress) await currency.save() @@ -56,12 +58,14 @@ const LP_TOKENS_MIGRATION_DATE = '2024-08-07' export const handleEvmTransfer = errorHandler(_handleEvmTransfer) async function _handleEvmTransfer(event: TransferLog): Promise { + if (!event.args) throw new Error('Missing event arguments') const [fromEvmAddress, toEvmAddress, amount] = event.args logger.info(`Transfer ${fromEvmAddress}-${toEvmAddress} of ${amount.toString()} at block: ${event.blockNumber}`) const timestamp = new Date(Number(event.block.timestamp) * 1000) const evmTokenAddress = event.address - const chainId = await getNodeEvmChainId() //(await networkPromise).chainId.toString(10) + const chainId = await getNodeEvmChainId() + if (!chainId) throw new Error('Unable to retrieve chainId') const evmBlockchain = await BlockchainService.getOrInit(chainId) const evmToken = await CurrencyService.getOrInitEvm(evmBlockchain.id, evmTokenAddress) const { escrowAddress, userEscrowAddress } = evmToken @@ -72,6 +76,7 @@ async function _handleEvmTransfer(event: TransferLog): Promise { const isFromEscrow = fromEvmAddress === escrowAddress const _isFromUserEscrow = fromEvmAddress === userEscrowAddress + if (!evmToken.poolId || !evmToken.trancheId) throw new Error('This is not a tranche token') const trancheId = evmToken.trancheId.split('-')[1] const tranche = await TrancheService.getById(evmToken.poolId, trancheId) @@ -87,15 +92,13 @@ async function _handleEvmTransfer(event: TransferLog): Promise { const isLpTokenMigrationDay = chainId === '1' && orderData.timestamp.toISOString().startsWith(LP_TOKENS_MIGRATION_DATE) - let fromAddress: string = null, - fromAccount: AccountService = null + let fromAddress: string, fromAccount: AccountService if (isFromUserAddress) { fromAddress = AccountService.evmToSubstrate(fromEvmAddress, evmBlockchain.id) fromAccount = await AccountService.getOrInit(fromAddress) } - let toAddress: string = null, - toAccount: AccountService = null + let toAddress: string, toAccount: AccountService if (isToUserAddress) { toAddress = AccountService.evmToSubstrate(toEvmAddress, evmBlockchain.id) toAccount = await AccountService.getOrInit(toAddress) @@ -103,23 +106,23 @@ async function _handleEvmTransfer(event: TransferLog): Promise { // Handle Currency Balance Updates if (isToUserAddress) { - const toBalance = await CurrencyBalanceService.getOrInit(toAddress, evmToken.id) + const toBalance = await CurrencyBalanceService.getOrInit(toAddress!, evmToken.id) await toBalance.credit(amount.toBigInt()) await toBalance.save() } if (isFromUserAddress) { - const fromBalance = await CurrencyBalanceService.getOrInit(fromAddress, evmToken.id) + const fromBalance = await CurrencyBalanceService.getOrInit(fromAddress!, evmToken.id) await fromBalance.debit(amount.toBigInt()) await fromBalance.save() } // Handle INVEST_LP_COLLECT if (isFromEscrow && isToUserAddress) { - const investLpCollect = InvestorTransactionService.collectLpInvestOrder({ ...orderData, address: toAccount.id }) + const investLpCollect = InvestorTransactionService.collectLpInvestOrder({ ...orderData, address: toAccount!.id }) await investLpCollect.save() - const trancheBalance = await TrancheBalanceService.getOrInit(toAccount.id, orderData.poolId, orderData.trancheId) + const trancheBalance = await TrancheBalanceService.getOrInit(toAccount!.id, orderData.poolId, orderData.trancheId) await trancheBalance.investCollect(orderData.amount) await trancheBalance.save() } @@ -133,7 +136,7 @@ async function _handleEvmTransfer(event: TransferLog): Promise { await tranche.loadSnapshot(getPeriodStart(timestamp)) const price = tranche.tokenPrice - const txIn = InvestorTransactionService.transferIn({ ...orderData, address: toAccount.id, price }) + const txIn = InvestorTransactionService.transferIn({ ...orderData, address: toAccount!.id, price }) await txIn.save() if (!isLpTokenMigrationDay) try { @@ -142,22 +145,22 @@ async function _handleEvmTransfer(event: TransferLog): Promise { txIn.trancheId, txIn.hash, txIn.timestamp, - txIn.tokenAmount, - txIn.tokenPrice + txIn.tokenAmount!, + txIn.tokenPrice! ) } catch (error) { logger.error(`Unable to save buy investor position: ${error}`) // TODO: Fallback use PoolManager Contract to read price } - const txOut = InvestorTransactionService.transferOut({ ...orderData, address: fromAccount.id, price }) + const txOut = InvestorTransactionService.transferOut({ ...orderData, address: fromAccount!.id, price }) if (!isLpTokenMigrationDay) { try { const profit = await InvestorPositionService.sellFifo( txOut.accountId, txOut.trancheId, - txOut.tokenAmount, - txOut.tokenPrice + txOut.tokenAmount!, + txOut.tokenPrice! ) await txOut.setRealizedProfitFifo(profit) } catch (error) { diff --git a/src/mappings/handlers/investmentsHandlers.ts b/src/mappings/handlers/investmentsHandlers.ts index f4797263..a8f61e9c 100644 --- a/src/mappings/handlers/investmentsHandlers.ts +++ b/src/mappings/handlers/investmentsHandlers.ts @@ -8,10 +8,13 @@ import { OutstandingOrderService } from '../services/outstandingOrderService' import { InvestorTransactionData, InvestorTransactionService } from '../services/investorTransactionService' import { AccountService } from '../services/accountService' import { TrancheBalanceService } from '../services/trancheBalanceService' +import { assertPropInitialized } from '../../helpers/validation' export const handleInvestOrderUpdated = errorHandler(_handleInvestOrderUpdated) async function _handleInvestOrderUpdated(event: SubstrateEvent): Promise { const [investmentId, , address, newAmount] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const poolId = Array.isArray(investmentId) ? investmentId[0] : investmentId.poolId const trancheId = Array.isArray(investmentId) ? investmentId[1] : investmentId.trancheId logger.info( @@ -29,6 +32,7 @@ async function _handleInvestOrderUpdated(event: SubstrateEvent BigInt(0)) { @@ -62,7 +66,9 @@ async function _handleInvestOrderUpdated(event: SubstrateEvent): Promise { const [investmentId, , address, amount] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const poolId = Array.isArray(investmentId) ? investmentId[0] : investmentId.poolId const trancheId = Array.isArray(investmentId) ? investmentId[1] : investmentId.trancheId logger.info( @@ -92,6 +100,7 @@ async function _handleRedeemOrderUpdated(event: SubstrateEvent BigInt(0)) { @@ -125,8 +134,10 @@ async function _handleRedeemOrderUpdated(event: SubstrateEvent): Promise { const [investmentId, address, , investCollection] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const poolId = Array.isArray(investmentId) ? investmentId[0] : investmentId.poolId const trancheId = Array.isArray(investmentId) ? investmentId[1] : investmentId.trancheId + if (!event.extrinsic) throw new Error('Missing event extrinsic') logger.info( `Orders collected for tranche ${poolId.toString()}-${trancheId.toString()}. ` + `Address: ${address.toHex()} at ` + @@ -164,13 +178,13 @@ async function _handleInvestOrdersCollected(event: SubstrateEvent): Promise { const [investmentId, address, , redeemCollection] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const poolId = Array.isArray(investmentId) ? investmentId[0] : investmentId.poolId const trancheId = Array.isArray(investmentId) ? investmentId[1] : investmentId.trancheId + if (!event.extrinsic) throw new Error('Missing event extrinsic') logger.info( `Orders collected for tranche ${poolId.toString()}-${trancheId.toString()}. ` + `Address: ${address.toHex()} ` + @@ -216,13 +233,13 @@ async function _handleRedeemOrdersCollected(event: SubstrateEvent) { const [poolId, loanId, loanInfo] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info(`Loan created event for pool: ${poolId.toString()} loan: ${loanId.toString()}`) const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool + if (!event.extrinsic) throw new Error('Missing event extrinsic!') const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const isInternal = loanInfo.pricing.isInternal - const internalLoanPricing = isInternal ? loanInfo.pricing.asInternal : null - const externalLoanPricing = !isInternal ? loanInfo.pricing.asExternal : null + const internalLoanPricing = isInternal ? loanInfo.pricing.asInternal : undefined + const externalLoanPricing = !isInternal ? loanInfo.pricing.asExternal : undefined const assetType: AssetType = - isInternal && internalLoanPricing.valuationMethod.isCash ? AssetType.OffchainCash : AssetType.Other + isInternal && internalLoanPricing!.valuationMethod.isCash ? AssetType.OffchainCash : AssetType.Other const valuationMethod: AssetValuationMethod = isInternal - ? AssetValuationMethod[internalLoanPricing.valuationMethod.type] + ? AssetValuationMethod[internalLoanPricing!.valuationMethod.type] : AssetValuationMethod.Oracle const asset = await AssetService.init( @@ -50,32 +54,32 @@ async function _handleLoanCreated(event: SubstrateEvent) { valuationMethod, loanInfo.collateral[0].toBigInt(), loanInfo.collateral[1].toBigInt(), - event.block.timestamp + timestamp ) - const assetSpecs = { + const assetSpecs: AssetSpecs = { advanceRate: internalLoanPricing && internalLoanPricing.maxBorrowAmount.isUpToOutstandingDebt ? internalLoanPricing.maxBorrowAmount.asUpToOutstandingDebt.advanceRate.toBigInt() - : null, - collateralValue: internalLoanPricing ? internalLoanPricing.collateralValue.toBigInt() : null, + : undefined, + collateralValue: internalLoanPricing ? internalLoanPricing.collateralValue.toBigInt() : undefined, probabilityOfDefault: internalLoanPricing && internalLoanPricing.valuationMethod.isDiscountedCashFlow ? internalLoanPricing.valuationMethod.asDiscountedCashFlow.probabilityOfDefault.toBigInt() - : null, + : undefined, lossGivenDefault: internalLoanPricing && internalLoanPricing.valuationMethod.isDiscountedCashFlow ? internalLoanPricing.valuationMethod.asDiscountedCashFlow.lossGivenDefault.toBigInt() - : null, + : undefined, discountRate: internalLoanPricing?.valuationMethod.isDiscountedCashFlow && internalLoanPricing.valuationMethod.asDiscountedCashFlow.discountRate.isFixed ? internalLoanPricing.valuationMethod.asDiscountedCashFlow.discountRate.asFixed.ratePerYear.toBigInt() - : null, + : undefined, maturityDate: loanInfo.schedule.maturity.isFixed ? new Date(loanInfo.schedule.maturity.asFixed.date.toNumber() * 1000) - : null, - notional: !isInternal ? externalLoanPricing.notional.toBigInt() : null, + : undefined, + notional: !isInternal ? externalLoanPricing!.notional.toBigInt() : undefined, } await asset.updateAssetSpecs(assetSpecs) @@ -83,7 +87,8 @@ async function _handleLoanCreated(event: SubstrateEvent) { await asset.updateIpfsAssetName() await asset.save() - const epoch = await EpochService.getById(pool.id, pool.currentEpoch) + assertPropInitialized(pool, 'currentEpoch', 'number') + const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) if (!epoch) throw new Error('Epoch not found!') const at = await AssetTransactionService.created({ @@ -92,7 +97,7 @@ async function _handleLoanCreated(event: SubstrateEvent) { address: account.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp, }) await at.save() @@ -107,6 +112,8 @@ async function _handleLoanCreated(event: SubstrateEvent) { export const handleLoanBorrowed = errorHandler(_handleLoanBorrowed) async function _handleLoanBorrowed(event: SubstrateEvent): Promise { const [poolId, loanId, borrowAmount] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const specVersion = api.runtimeVersion.specVersion.toNumber() const pool = await PoolService.getById(poolId.toString()) @@ -116,9 +123,11 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr logger.info(`Loan borrowed event for pool: ${poolId.toString()} amount: ${amount.toString()}`) + if (!event.extrinsic) throw new Error('Missing event extrinsic!') const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) - const epoch = await EpochService.getById(pool.id, pool.currentEpoch) + assertPropInitialized(pool, 'currentEpoch', 'number') + const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) if (!epoch) throw new Error('Epoch not found!') // Update loan amount @@ -131,11 +140,11 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr address: account.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp, amount: amount, principalAmount: amount, - quantity: borrowAmount.isExternal ? borrowAmount.asExternal.quantity.toBigInt() : null, - settlementPrice: borrowAmount.isExternal ? borrowAmount.asExternal.settlementPrice.toBigInt() : null, + quantity: borrowAmount.isExternal ? borrowAmount.asExternal.quantity.toBigInt() : undefined, + settlementPrice: borrowAmount.isExternal ? borrowAmount.asExternal.settlementPrice.toBigInt() : undefined, } if (asset.isOffchainCash()) { @@ -157,8 +166,8 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr asset.id, assetTransactionBaseData.hash, assetTransactionBaseData.timestamp, - assetTransactionBaseData.quantity, - assetTransactionBaseData.settlementPrice + assetTransactionBaseData.quantity!, + assetTransactionBaseData.settlementPrice! ) } @@ -182,6 +191,8 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr export const handleLoanRepaid = errorHandler(_handleLoanRepaid) async function _handleLoanRepaid(event: SubstrateEvent) { const [poolId, loanId, { principal, interest, unscheduled }] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const specVersion = api.runtimeVersion.specVersion.toNumber() const pool = await PoolService.getById(poolId.toString()) @@ -192,26 +203,27 @@ async function _handleLoanRepaid(event: SubstrateEvent) { logger.info(`Loan repaid event for pool: ${poolId.toString()} amount: ${amount.toString()}`) + if (!event.extrinsic) throw new Error('Missing event extrinsic!') const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) - const epoch = await EpochService.getById(pool.id, pool.currentEpoch) + assertPropInitialized(pool, 'currentEpoch', 'number') + const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) if (!epoch) throw new Error('Epoch not found!') const asset = await AssetService.getById(poolId.toString(), loanId.toString()) - const assetTransactionBaseData = { poolId: poolId.toString(), assetId: loanId.toString(), address: account.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp, amount: amount, principalAmount: principalAmount, interestAmount: interest.toBigInt(), unscheduledAmount: unscheduled.toBigInt(), - quantity: principal.isExternal ? principal.asExternal.quantity.toBigInt() : null, - settlementPrice: principal.isExternal ? principal.asExternal.settlementPrice.toBigInt() : null, + quantity: principal.isExternal ? principal.asExternal.quantity.toBigInt() : undefined, + settlementPrice: principal.isExternal ? principal.asExternal.settlementPrice.toBigInt() : undefined, } if (asset.isOffchainCash()) { @@ -241,7 +253,10 @@ async function _handleLoanRepaid(event: SubstrateEvent) { await pool.increaseRealizedProfitFifo(realizedProfitFifo) } - const at = await AssetTransactionService.repaid({ ...assetTransactionBaseData, realizedProfitFifo }) + const at = await AssetTransactionService.repaid({ + ...assetTransactionBaseData, + realizedProfitFifo: realizedProfitFifo!, + }) await at.save() // Update pool info @@ -271,7 +286,7 @@ async function _handleLoanWrittenOff(event: SubstrateEvent) const pool = await PoolService.getById(poolId.toString()) if (pool === undefined) throw missingPool - await pool.increaseWriteOff(asset.writtenOffAmountByPeriod) + await pool.increaseWriteOff(asset.writtenOffAmountByPeriod!) await pool.save() // Record cashflows @@ -281,18 +296,22 @@ async function _handleLoanWrittenOff(event: SubstrateEvent) export const handleLoanClosed = errorHandler(_handleLoanClosed) async function _handleLoanClosed(event: SubstrateEvent) { const [poolId, loanId] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info(`Loan closed event for pool: ${poolId.toString()} loanId: ${loanId.toString()}`) const pool = await PoolService.getById(poolId.toString()) if (pool === undefined) throw missingPool + if (!event.extrinsic) throw new Error('Missing event extrinsic!') const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const asset = await AssetService.getById(poolId.toString(), loanId.toString()) await asset.close() await asset.save() - const epoch = await EpochService.getById(pool.id, pool.currentEpoch) + assertPropInitialized(pool, 'currentEpoch', 'number') + const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) if (!epoch) throw new Error('Epoch not found!') const at = await AssetTransactionService.closed({ @@ -301,7 +320,7 @@ async function _handleLoanClosed(event: SubstrateEvent) { address: account.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp, }) await at.save() @@ -313,6 +332,10 @@ export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred async function _handleLoanDebtTransferred(event: SubstrateEvent) { const specVersion = api.runtimeVersion.specVersion.toNumber() const [poolId, fromLoanId, toLoanId, _repaidAmount, _borrowAmount] = event.event.data + + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) + const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -329,12 +352,14 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent) { const [poolId, fromLoanId, toLoanId, _amount] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) + const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -469,12 +497,14 @@ async function _handleLoanDebtTransferred1024(event: SubstrateEvent) { const [poolId, loanId, _borrowAmount] = event.event.data + + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) + const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -564,11 +598,13 @@ async function _handleLoanDebtIncreased(event: SubstrateEvent `amount: ${borrowPrincipalAmount.toString()}` ) + if (!event.extrinsic) throw new Error('Missing event extrinsic!') const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const asset = await AssetService.getById(poolId.toString(), loanId.toString()) - const epoch = await EpochService.getById(pool.id, pool.currentEpoch) + assertPropInitialized(pool, 'currentEpoch', 'number') + const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) if (!epoch) throw new Error('Epoch not found!') const txData: AssetTransactionData = { @@ -576,12 +612,12 @@ async function _handleLoanDebtIncreased(event: SubstrateEvent address: account.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp, assetId: loanId.toString(10), amount: borrowPrincipalAmount, principalAmount: borrowPrincipalAmount, - quantity: _borrowAmount.isExternal ? _borrowAmount.asExternal.quantity.toBigInt() : null, - settlementPrice: _borrowAmount.isExternal ? _borrowAmount.asExternal.settlementPrice.toBigInt() : null, + quantity: _borrowAmount.isExternal ? _borrowAmount.asExternal.quantity.toBigInt() : undefined, + settlementPrice: _borrowAmount.isExternal ? _borrowAmount.asExternal.settlementPrice.toBigInt() : undefined, } //TODO: should be tracked separately as corrections @@ -600,6 +636,9 @@ async function _handleLoanDebtIncreased(event: SubstrateEvent export const handleLoanDebtDecreased = errorHandler(_handleLoanDebtDecreased) async function _handleLoanDebtDecreased(event: SubstrateEvent) { const [poolId, loanId, _repaidAmount] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) + const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -613,11 +652,13 @@ async function _handleLoanDebtDecreased(event: SubstrateEvent `amount: ${repaidAmount.toString()}` ) + if (!event.extrinsic) throw new Error('Missing event extrinsic!') const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const asset = await AssetService.getById(poolId.toString(), loanId.toString()) - const epoch = await EpochService.getById(pool.id, pool.currentEpoch) + assertPropInitialized(pool, 'currentEpoch', 'number') + const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) if (!epoch) throw new Error('Epoch not found!') const txData: AssetTransactionData = { @@ -625,16 +666,16 @@ async function _handleLoanDebtDecreased(event: SubstrateEvent address: account.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp: timestamp, assetId: loanId.toString(10), amount: repaidAmount, interestAmount: repaidInterestAmount, principalAmount: repaidPrincipalAmount, unscheduledAmount: repaidUnscheduledAmount, - quantity: _repaidAmount.principal.isExternal ? _repaidAmount.principal.asExternal.quantity.toBigInt() : null, + quantity: _repaidAmount.principal.isExternal ? _repaidAmount.principal.asExternal.quantity.toBigInt() : undefined, settlementPrice: _repaidAmount.principal.isExternal ? _repaidAmount.principal.asExternal.settlementPrice.toBigInt() - : null, + : undefined, } //Track repayment diff --git a/src/mappings/handlers/oracleHandlers.ts b/src/mappings/handlers/oracleHandlers.ts index 2adc9e30..bc6bcc91 100644 --- a/src/mappings/handlers/oracleHandlers.ts +++ b/src/mappings/handlers/oracleHandlers.ts @@ -6,9 +6,9 @@ import { OracleTransactionData, OracleTransactionService } from '../services/ora export const handleOracleFed = errorHandler(_handleOracleFed) async function _handleOracleFed(event: SubstrateEvent) { const [feeder, key, value] = event.event.data - + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) let formattedKey: string - switch (key.type) { case 'Isin': { formattedKey = key.asIsin.toUtf8() @@ -20,15 +20,16 @@ async function _handleOracleFed(event: SubstrateEvent) { break } default: - logger.warn(`Oracle feed: ${feeder.toString()} key: ${formattedKey} value: ${value.toString()}`) + logger.warn(`Oracle feed: ${feeder.toString()} key: ${key.type.toString()} value: ${value.toString()}`) return } logger.info(`Oracle feeder: ${feeder.toString()} key: ${formattedKey} value: ${value.toString()}`) + if (!event.extrinsic) throw new Error('Missing event extrinsic') const oracleTxData: OracleTransactionData = { hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp, key: formattedKey, value: value.toBigInt(), } diff --git a/src/mappings/handlers/ormlTokensHandlers.ts b/src/mappings/handlers/ormlTokensHandlers.ts index 931b108d..cb05b90a 100644 --- a/src/mappings/handlers/ormlTokensHandlers.ts +++ b/src/mappings/handlers/ormlTokensHandlers.ts @@ -13,6 +13,8 @@ import { InvestorPositionService } from '../services/investorPositionService' export const handleTokenTransfer = errorHandler(_handleTokenTransfer) async function _handleTokenTransfer(event: SubstrateEvent): Promise { const [_currency, from, to, amount] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error('Timestamp missing from block') // Skip token transfers from and to excluded addresses const fromAddress = String.fromCharCode(...from.toU8a()) @@ -36,12 +38,12 @@ async function _handleTokenTransfer(event: SubstrateEvent): // TRANCHE TOKEN TRANSFERS BETWEEN INVESTORS if (_currency.isTranche && !isFromExcludedAddress && !isToExcludedAddress) { - const pool = await PoolService.getById(_currency.asTranche[0].toString()) + const poolId = Array.isArray(_currency.asTranche) ? _currency.asTranche[0] : _currency.asTranche.poolId + const trancheId = Array.isArray(_currency.asTranche) ? _currency.asTranche[1] : _currency.asTranche.trancheId + const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool - - const tranche = await TrancheService.getById(pool.id, _currency.asTranche[1].toString()) - if (!tranche) throw missingPool - + const tranche = await TrancheService.getById(pool.id, trancheId.toString()) + if (!tranche) throw Error('Tranche not found!') logger.info( `Tranche Token transfer between investors tor tranche: ${pool.id}-${tranche.trancheId}. ` + `from: ${from.toHex()} to: ${to.toHex()} amount: ${amount.toString()} ` + @@ -51,13 +53,13 @@ async function _handleTokenTransfer(event: SubstrateEvent): // Update tranche price await tranche.updatePriceFromRuntime(event.block.block.header.number.toNumber()) await tranche.save() - + if (!event.extrinsic) throw new Error('Missing extrinsic in event') const orderData = { poolId: pool.id, trancheId: tranche.trancheId, epochNumber: pool.currentEpoch, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp: timestamp, price: tranche.tokenPrice, amount: amount.toBigInt(), } @@ -68,8 +70,8 @@ async function _handleTokenTransfer(event: SubstrateEvent): const profit = await InvestorPositionService.sellFifo( txOut.accountId, txOut.trancheId, - txOut.tokenAmount, - txOut.tokenPrice + txOut.tokenAmount!, + txOut.tokenPrice! ) await txOut.setRealizedProfitFifo(profit) await txOut.save() @@ -81,8 +83,8 @@ async function _handleTokenTransfer(event: SubstrateEvent): txIn.trancheId, txIn.hash, txIn.timestamp, - txIn.tokenAmount, - txIn.tokenPrice + txIn.tokenAmount!, + txIn.tokenPrice! ) await txIn.save() } diff --git a/src/mappings/handlers/poolFeesHandlers.ts b/src/mappings/handlers/poolFeesHandlers.ts index ae7a2d2b..4aed3025 100644 --- a/src/mappings/handlers/poolFeesHandlers.ts +++ b/src/mappings/handlers/poolFeesHandlers.ts @@ -16,27 +16,29 @@ import { EpochService } from '../services/epochService' export const handleFeeProposed = errorHandler(_handleFeeProposed) async function _handleFeeProposed(event: SubstrateEvent): Promise { const [poolId, feeId, _bucket, fee] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Fee with id ${feeId.toString(10)} proposed for pool ${poolId.toString(10)} ` + `on block ${event.block.block.header.number.toNumber()}` ) const pool = await PoolService.getOrSeed(poolId.toString(10), true, true) const epoch = await epochFetcher(pool) + if (!epoch) throw new Error('Epoch not found') const poolFeeData: PoolFeeData = { poolId: pool.id, feeId: feeId.toString(10), blockNumber: event.block.block.header.number.toNumber(), - timestamp: event.block.timestamp, + timestamp, epochId: epoch.id, hash: event.hash.toString(), } const type = fee.feeType.type - const poolFee = await PoolFeeService.propose(poolFeeData, type) await poolFee.setName( await pool.getIpfsPoolFeeName(poolFee.feeId).catch((err) => { logger.error(`IPFS Request failed ${err}`) - return Promise.resolve(null) + return Promise.resolve('') }) ) await poolFee.save() @@ -48,17 +50,20 @@ async function _handleFeeProposed(event: SubstrateEvent): export const handleFeeAdded = errorHandler(_handleFeeAdded) async function _handleFeeAdded(event: SubstrateEvent): Promise { const [poolId, _bucket, feeId, fee] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Fee with id ${feeId.toString(10)} added for pool ${poolId.toString(10)} ` + `on block ${event.block.block.header.number.toNumber()}` ) const pool = await PoolService.getOrSeed(poolId.toString(10), true, true) const epoch = await epochFetcher(pool) + if (!epoch) throw new Error('Epoch not found') const poolFeeData: PoolFeeData = { poolId: pool.id, feeId: feeId.toString(10), blockNumber: event.block.block.header.number.toNumber(), - timestamp: event.block.timestamp, + timestamp, epochId: epoch.id, hash: event.hash.toString(), } @@ -68,7 +73,7 @@ async function _handleFeeAdded(event: SubstrateEvent): Promi await poolFee.setName( await pool.getIpfsPoolFeeName(poolFee.feeId).catch((err) => { logger.error(`IPFS Request failed ${err}`) - return Promise.resolve(null) + return Promise.resolve('') }) ) await poolFee.save() @@ -80,6 +85,8 @@ async function _handleFeeAdded(event: SubstrateEvent): Promi export const handleFeeRemoved = errorHandler(_handleFeeRemoved) async function _handleFeeRemoved(event: SubstrateEvent): Promise { const [poolId, _bucket, feeId] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Fee with id ${feeId.toString(10)} removed for pool ${poolId.toString(10)} ` + `on block ${event.block.block.header.number.toNumber()}` @@ -87,11 +94,12 @@ async function _handleFeeRemoved(event: SubstrateEvent): P const pool = await PoolService.getById(poolId.toString(10)) if (!pool) throw missingPool const epoch = await epochFetcher(pool) + if (!epoch) throw new Error('Epoch not found') const poolFeeData: PoolFeeData = { poolId: pool.id, feeId: feeId.toString(10), blockNumber: event.block.block.header.number.toNumber(), - timestamp: event.block.timestamp, + timestamp: timestamp, epochId: epoch.id, hash: event.hash.toString(), } @@ -106,6 +114,8 @@ async function _handleFeeRemoved(event: SubstrateEvent): P export const handleFeeCharged = errorHandler(_handleFeeCharged) async function _handleFeeCharged(event: SubstrateEvent): Promise { const [poolId, feeId, amount, pending] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Fee with id ${feeId.toString(10)} charged for pool ${poolId.toString(10)} ` + `on block ${event.block.block.header.number.toNumber()}` @@ -113,11 +123,12 @@ async function _handleFeeCharged(event: SubstrateEvent): P const pool = await PoolService.getById(poolId.toString(10)) if (!pool) throw missingPool const epoch = await epochFetcher(pool) + if (!epoch) throw new Error('Epoch not found') const poolFeeData = { poolId: pool.id, feeId: feeId.toString(10), blockNumber: event.block.block.header.number.toNumber(), - timestamp: event.block.timestamp, + timestamp: timestamp, epochId: epoch.id, hash: event.hash.toString(), amount: amount.toBigInt(), @@ -139,6 +150,8 @@ async function _handleFeeCharged(event: SubstrateEvent): P export const handleFeeUncharged = errorHandler(_handleFeeUncharged) async function _handleFeeUncharged(event: SubstrateEvent): Promise { const [poolId, feeId, amount, pending] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Fee with id ${feeId.toString(10)} uncharged for pool ${poolId.toString(10)} ` + `on block ${event.block.block.header.number.toNumber()}` @@ -146,11 +159,12 @@ async function _handleFeeUncharged(event: SubstrateEvent const pool = await PoolService.getById(poolId.toString(10)) if (!pool) throw missingPool const epoch = await epochFetcher(pool) + if (!epoch) throw new Error('Epoch not found') const poolFeeData = { poolId: pool.id, feeId: feeId.toString(10), blockNumber: event.block.block.header.number.toNumber(), - timestamp: event.block.timestamp, + timestamp, epochId: epoch.id, hash: event.hash.toString(), amount: amount.toBigInt(), @@ -172,6 +186,8 @@ async function _handleFeeUncharged(event: SubstrateEvent export const handleFeePaid = errorHandler(_handleFeePaid) async function _handleFeePaid(event: SubstrateEvent): Promise { const [poolId, feeId, amount, _destination] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Fee with id ${feeId.toString(10)} paid for pool ${poolId.toString(10)} ` + `on block ${event.block.block.header.number.toNumber()}` @@ -179,11 +195,12 @@ async function _handleFeePaid(event: SubstrateEvent): Promise const pool = await PoolService.getById(poolId.toString(10)) if (!pool) throw missingPool const epoch = await epochFetcher(pool) + if (!epoch) throw new Error('Epoch not found') const poolFeeData = { poolId: pool.id, feeId: feeId.toString(10), blockNumber: event.block.block.header.number.toNumber(), - timestamp: event.block.timestamp, + timestamp, epochId: epoch.id, hash: event.hash.toString(), amount: amount.toBigInt(), @@ -207,8 +224,8 @@ async function _handleFeePaid(event: SubstrateEvent): Promise function epochFetcher(pool: PoolService) { const { lastEpochClosed, lastEpochExecuted, currentEpoch } = pool if (lastEpochClosed === lastEpochExecuted) { - return EpochService.getById(pool.id, currentEpoch) + return EpochService.getById(pool.id, currentEpoch!) } else { - return EpochService.getById(pool.id, lastEpochClosed) + return EpochService.getById(pool.id, lastEpochClosed!) } } diff --git a/src/mappings/handlers/poolsHandlers.ts b/src/mappings/handlers/poolsHandlers.ts index 9f3c88cf..aef9d296 100644 --- a/src/mappings/handlers/poolsHandlers.ts +++ b/src/mappings/handlers/poolsHandlers.ts @@ -15,10 +15,13 @@ import { substrateStateSnapshotter } from '../../helpers/stateSnapshot' import { Pool, PoolSnapshot } from '../../types' import { InvestorPositionService } from '../services/investorPositionService' import { PoolFeeService } from '../services/poolFeeService' +import { assertPropInitialized } from '../../helpers/validation' export const handlePoolCreated = errorHandler(_handlePoolCreated) async function _handlePoolCreated(event: SubstrateEvent): Promise { const [, , poolId, essence] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) const formattedCurrency = `${LOCAL_CHAIN_ID}-${essence.currency.type}-` + `${currencyFormatters[essence.currency.type](essence.currency.value).join('-')}` @@ -41,7 +44,7 @@ async function _handlePoolCreated(event: SubstrateEvent): Prom essence.maxReserve.toBigInt(), essence.maxNavAge.toNumber(), essence.minEpochTime.toNumber(), - event.block.timestamp, + timestamp, event.block.block.header.number.toNumber() ) await pool.initData() @@ -81,11 +84,12 @@ async function _handlePoolCreated(event: SubstrateEvent): Prom } // Initialise Epoch + assertPropInitialized(pool, 'currentEpoch', 'number') const trancheIds = tranches.map((tranche) => tranche.trancheId) - const epoch = await EpochService.init(pool.id, pool.currentEpoch, trancheIds, event.block.timestamp) + const epoch = await EpochService.init(pool.id, pool.currentEpoch!, trancheIds, timestamp) await epoch.saveWithStates() - const onChainCashAsset = AssetService.initOnchainCash(pool.id, event.block.timestamp) + const onChainCashAsset = AssetService.initOnchainCash(pool.id, timestamp) await onChainCashAsset.save() logger.info(`Pool ${pool.id} successfully created!`) } @@ -142,25 +146,23 @@ async function _handleMetadataSet(event: SubstrateEvent) { export const handleEpochClosed = errorHandler(_handleEpochClosed) async function _handleEpochClosed(event: SubstrateEvent): Promise { const [poolId, epochId] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Epoch ${epochId.toNumber()} closed for pool ${poolId.toString()} in block ${event.block.block.header.number}` ) const pool = await PoolService.getById(poolId.toString()) - if (pool === undefined) throw missingPool + if (!pool) throw missingPool // Close the current epoch and open a new one const tranches = await TrancheService.getActivesByPoolId(poolId.toString()) const epoch = await EpochService.getById(poolId.toString(), epochId.toNumber()) - await epoch.closeEpoch(event.block.timestamp) + if (!epoch) throw new Error(`Epoch ${epochId.toString(10)} not found for pool ${poolId.toString(10)}`) + await epoch.closeEpoch(timestamp) await epoch.saveWithStates() const trancheIds = tranches.map((tranche) => tranche.trancheId) - const nextEpoch = await EpochService.init( - poolId.toString(), - epochId.toNumber() + 1, - trancheIds, - event.block.timestamp - ) + const nextEpoch = await EpochService.init(poolId.toString(), epochId.toNumber() + 1, trancheIds, timestamp) await nextEpoch.saveWithStates() await pool.closeEpoch(epochId.toNumber()) @@ -170,6 +172,8 @@ async function _handleEpochClosed(event: SubstrateEvent): Promise { const [poolId, epochId] = event.event.data + const timestamp = event.block.timestamp + if (!timestamp) throw new Error(`Block ${event.block.block.header.number.toString()} has no timestamp`) logger.info( `Epoch ${epochId.toString()} executed event for pool ${poolId.toString()} ` + `at block ${event.block.block.header.number.toString()}` @@ -179,37 +183,40 @@ async function _handleEpochExecuted(event: SubstrateEvent epochState.trancheId === tranche.trancheId) + if (!epochState) throw new Error('EpochState not found!') await tranche.updateSupply() - await tranche.updatePrice(epochState.tokenPrice, event.block.block.header.number.toNumber()) - await tranche.updateFulfilledInvestOrders(epochState.sumFulfilledInvestOrders) - await tranche.updateFulfilledRedeemOrders(epochState.sumFulfilledRedeemOrders) + await tranche.updatePrice(epochState.tokenPrice!, event.block.block.header.number.toNumber()) + await tranche.updateFulfilledInvestOrders(epochState.sumFulfilledInvestOrders!) + await tranche.updateFulfilledRedeemOrders(epochState.sumFulfilledRedeemOrders!) await tranche.save() // Carry over aggregated unfulfilled orders to next epoch await nextEpoch.updateOutstandingInvestOrders( tranche.trancheId, - epochState.sumOutstandingInvestOrders - epochState.sumFulfilledInvestOrders, + epochState.sumOutstandingInvestOrders! - epochState.sumFulfilledInvestOrders!, BigInt(0) ) await nextEpoch.updateOutstandingRedeemOrders( tranche.trancheId, - epochState.sumOutstandingRedeemOrders - epochState.sumFulfilledRedeemOrders, + epochState.sumOutstandingRedeemOrders! - epochState.sumFulfilledRedeemOrders!, BigInt(0), - epochState.tokenPrice + epochState.tokenPrice! ) // Find single outstanding orders posted for this tranche and fulfill them to investorTransactions @@ -225,7 +232,7 @@ async function _handleEpochExecuted(event: SubstrateEvent BigInt(0) && epochState.investFulfillmentPercentage > BigInt(0)) { + if (oo.investAmount > BigInt(0) && epochState.investFulfillmentPercentage! > BigInt(0)) { const it = InvestorTransactionService.executeInvestOrder({ ...orderData, amount: oo.investAmount, fulfillmentPercentage: epochState.investFulfillmentPercentage, }) await it.save() - await oo.updateUnfulfilledInvest(it.currencyAmount) - await trancheBalance.investExecute(it.currencyAmount, it.tokenAmount) + await oo.updateUnfulfilledInvest(it.currencyAmount!) + await trancheBalance.investExecute(it.currencyAmount!, it.tokenAmount!) await InvestorPositionService.buy( it.accountId, it.trancheId, it.hash, it.timestamp, - it.tokenAmount, - it.tokenPrice + it.tokenAmount!, + it.tokenPrice! ) } - if (oo.redeemAmount > BigInt(0) && epochState.redeemFulfillmentPercentage > BigInt(0)) { + if (oo.redeemAmount > BigInt(0) && epochState.redeemFulfillmentPercentage! > BigInt(0)) { const it = InvestorTransactionService.executeRedeemOrder({ ...orderData, amount: oo.redeemAmount, fulfillmentPercentage: epochState.redeemFulfillmentPercentage, }) - await oo.updateUnfulfilledRedeem(it.tokenAmount) - await trancheBalance.redeemExecute(it.tokenAmount, it.currencyAmount) + await oo.updateUnfulfilledRedeem(it.tokenAmount!) + await trancheBalance.redeemExecute(it.tokenAmount!, it.currencyAmount!) - const profit = await InvestorPositionService.sellFifo(it.accountId, it.trancheId, it.tokenAmount, it.tokenPrice) + const profit = await InvestorPositionService.sellFifo( + it.accountId, + it.trancheId, + it.tokenAmount!, + it.tokenPrice! + ) await it.setRealizedProfitFifo(profit) await it.save() } @@ -282,22 +294,23 @@ async function _handleEpochExecuted(event: SubstrateEvent = { poolId: pool.id, epochNumber: epoch.index, hash: event.extrinsic.extrinsic.hash.toString(), - timestamp: event.block.timestamp, + timestamp: timestamp, assetId: ONCHAIN_CASH_ASSET_ID, } const assetTransactionSaves: Array> = [] - if (epoch.sumInvestedAmount > BigInt(0)) { + if (epoch.sumInvestedAmount! > BigInt(0)) { const deposit = AssetTransactionService.depositFromInvestments({ ...txData, amount: epoch.sumInvestedAmount }) assetTransactionSaves.push(deposit.save()) } - if (epoch.sumRedeemedAmount > BigInt(0)) { + if (epoch.sumRedeemedAmount! > BigInt(0)) { const withdrawalRedemptions = await AssetTransactionService.withdrawalForRedemptions({ ...txData, amount: epoch.sumRedeemedAmount, @@ -307,7 +320,7 @@ async function _handleEpochExecuted(event: SubstrateEvent BigInt(0)) { + if (epoch.sumPoolFeesPaidAmount! > BigInt(0)) { const withdrawalFees = await AssetTransactionService.withdrawalForFees({ ...txData, amount: epoch.sumPoolFeesPaidAmount, @@ -325,8 +338,7 @@ async function _handleEpochExecuted(event: SubstrateEvent { test('then reset accumulators are set to 0', () => { const resetAccumulators = Object.getOwnPropertyNames(loan).filter((prop) => prop.endsWith('ByPeriod')) for (const resetAccumulator of resetAccumulators) { - expect(loan[resetAccumulator]).toBe(BigInt(0)) + expect(loan[resetAccumulator as keyof typeof loan]).toBe(BigInt(0)) } }) diff --git a/src/mappings/services/assetService.ts b/src/mappings/services/assetService.ts index 5e33d44e..720c618e 100644 --- a/src/mappings/services/assetService.ts +++ b/src/mappings/services/assetService.ts @@ -5,6 +5,7 @@ import { ApiQueryLoansActiveLoans, LoanPricingAmount, NftItemMetadata } from '.. import { Asset, AssetType, AssetValuationMethod, AssetStatus, AssetSnapshot } from '../../types' import { ActiveLoanData } from './poolService' import { cid, readIpfs } from '../../helpers/ipfsFetch' +import { assertPropInitialized } from '../../helpers/validation' export const ONCHAIN_CASH_ASSET_ID = '0' export class AssetService extends Asset { @@ -86,30 +87,37 @@ export class AssetService extends Asset { static async getByNftId(collectionId: string, itemId: string) { const asset = ( - await AssetService.getByFields([ - ['collateralNftClassId', '=', collectionId], - ['collateralNftItemId', '=', itemId], - ], { limit: 100 }) + await AssetService.getByFields( + [ + ['collateralNftClassId', '=', collectionId], + ['collateralNftItemId', '=', itemId], + ], + { limit: 100 } + ) ).pop() as AssetService return asset } public borrow(amount: bigint) { logger.info(`Increasing borrowings for asset ${this.id} by ${amount}`) - this.borrowedAmountByPeriod += amount + assertPropInitialized(this, 'borrowedAmountByPeriod', 'bigint') + this.borrowedAmountByPeriod! += amount } public repay(amount: bigint) { logger.info(`Increasing repayments for asset ${this.id} by ${amount}`) - this.repaidAmountByPeriod += amount + assertPropInitialized(this, 'repaidAmountByPeriod', 'bigint') + this.repaidAmountByPeriod! += amount } public increaseQuantity(increase: bigint) { - this.outstandingQuantity += increase + assertPropInitialized(this, 'outstandingQuantity', 'bigint') + this.outstandingQuantity! += increase } public decreaseQuantity(decrease: bigint) { - this.outstandingQuantity -= decrease + assertPropInitialized(this, 'outstandingQuantity', 'bigint') + this.outstandingQuantity! -= decrease } public updateInterestRate(interestRatePerSec: bigint) { @@ -156,18 +164,25 @@ export class AssetService extends Asset { Object.assign(this, activeAssetData) if (this.snapshot) { - const deltaRepaidInterestAmount = this.totalRepaid - this.snapshot.totalRepaidInterest + assertPropInitialized(this, 'totalRepaid', 'bigint') + assertPropInitialized(this, 'totalRepaidInterest', 'bigint') + const deltaRepaidInterestAmount = this.totalRepaid! - this.snapshot.totalRepaidInterest! + + assertPropInitialized(this, 'outstandingInterest', 'bigint') + assertPropInitialized(this.snapshot, 'outstandingInterest', 'bigint') this.interestAccruedByPeriod = - this.outstandingInterest - this.snapshot.outstandingInterest + deltaRepaidInterestAmount - logger.info(`Updated outstanding debt for asset: ${this.id} to ${this.outstandingDebt.toString()}`) + this.outstandingInterest! - this.snapshot.outstandingInterest! + deltaRepaidInterestAmount + logger.info(`Updated outstanding interest for asset: ${this.id} to ${this.outstandingInterest!.toString()}`) } } public async updateItemMetadata() { + assertPropInitialized(this, 'collateralNftClassId', 'bigint') + assertPropInitialized(this, 'collateralNftItemId', 'bigint') logger.info( `Updating metadata for asset: ${this.id} with ` + - `collectionId ${this.collateralNftClassId.toString()}, ` + - `itemId: ${this.collateralNftItemId.toString()}` + `collectionId ${this.collateralNftClassId!.toString()}, ` + + `itemId: ${this.collateralNftItemId!.toString()}` ) const itemMetadata = await api.query.uniques.instanceMetadataOf>( this.collateralNftClassId, @@ -176,8 +191,8 @@ export class AssetService extends Asset { if (itemMetadata.isNone) { throw new Error( `No metadata returned for asset: ${this.id} with ` + - `collectionId ${this.collateralNftClassId.toString()}, ` + - `itemId: ${this.collateralNftItemId.toString()}` + `collectionId ${this.collateralNftClassId!.toString()}, ` + + `itemId: ${this.collateralNftItemId!.toString()}` ) } @@ -211,7 +226,10 @@ export class AssetService extends Asset { logger.warn(`No metadata field set for asset ${this.id}`) return } - const metadata: AssetIpfsMetadata = await readIpfs(this.metadata.match(cid)[0]).catch((err) => { + const matchedCid = this.metadata.match(cid) + if (!matchedCid || matchedCid.length !== 1) throw new Error(`Could not read stored fetadata for object ${this.id}`) + + const metadata = await readIpfs(matchedCid[0]).catch((err) => { logger.error(`Request for metadata failed: ${err}`) return undefined }) @@ -250,20 +268,25 @@ export class AssetService extends Asset { logger.info( `Updating unrealizedProfit for asset ${this.id} with atMarketPrice: ${atMarketPrice}, atNotional: ${atNotional}` ) - this.unrealizedProfitByPeriod = atMarketPrice - this.unrealizedProfitAtMarketPrice + assertPropInitialized(this, 'unrealizedProfitAtMarketPrice', 'bigint') + this.unrealizedProfitByPeriod = atMarketPrice - this.unrealizedProfitAtMarketPrice! this.unrealizedProfitAtMarketPrice = atMarketPrice this.unrealizedProfitAtNotional = atNotional } public increaseRealizedProfitFifo(increase: bigint) { - this.sumRealizedProfitFifo += increase + assertPropInitialized(this, 'sumRealizedProfitFifo', 'bigint') + this.sumRealizedProfitFifo! += increase } public async loadSnapshot(periodStart: Date) { - const snapshots = await AssetSnapshot.getByFields([ - ['assetId', '=', this.id], - ['periodId', '=', periodStart.toISOString()], - ], { limit: 100 }) + const snapshots = await AssetSnapshot.getByFields( + [ + ['assetId', '=', this.id], + ['periodId', '=', periodStart.toISOString()], + ], + { limit: 100 } + ) if (snapshots.length !== 1) { logger.warn(`Unable to load snapshot for asset ${this.id} for period ${periodStart.toISOString()}`) return @@ -276,9 +299,9 @@ export class AssetService extends Asset { } } -interface AssetSpecs { - advanceRate: bigint - collateralValue: bigint +export interface AssetSpecs { + advanceRate?: bigint + collateralValue?: bigint probabilityOfDefault?: bigint lossGivenDefault?: bigint discountRate?: bigint diff --git a/src/mappings/services/assetTransactionService.ts b/src/mappings/services/assetTransactionService.ts index d7518c39..a8c3416c 100644 --- a/src/mappings/services/assetTransactionService.ts +++ b/src/mappings/services/assetTransactionService.ts @@ -30,16 +30,16 @@ export class AssetTransactionService extends AssetTransaction { `${data.poolId}-${data.assetId}`, type ) - tx.accountId = data.address ?? null - tx.amount = data.amount ?? null - tx.principalAmount = data.principalAmount ?? null - tx.interestAmount = data.interestAmount ?? null - tx.unscheduledAmount = data.unscheduledAmount ?? null - tx.quantity = data.quantity ?? null - tx.settlementPrice = data.settlementPrice ?? null - tx.fromAssetId = data.fromAssetId ? `${data.poolId}-${data.fromAssetId}` : null - tx.toAssetId = data.toAssetId ? `${data.poolId}-${data.toAssetId}` : null - tx.realizedProfitFifo = data.realizedProfitFifo ?? null + tx.accountId = data.address + tx.amount = data.amount + tx.principalAmount = data.principalAmount + tx.interestAmount = data.interestAmount + tx.unscheduledAmount = data.unscheduledAmount + tx.quantity = data.quantity + tx.settlementPrice = data.settlementPrice + tx.fromAssetId = data.fromAssetId ? `${data.poolId}-${data.fromAssetId}` : undefined + tx.toAssetId = data.toAssetId ? `${data.poolId}-${data.toAssetId}` : undefined + tx.realizedProfitFifo = data.realizedProfitFifo return tx } diff --git a/src/mappings/services/currencyService.ts b/src/mappings/services/currencyService.ts index 7b6107f4..d58bd645 100644 --- a/src/mappings/services/currencyService.ts +++ b/src/mappings/services/currencyService.ts @@ -57,9 +57,9 @@ export class CurrencyService extends Currency { } export const currencyFormatters: CurrencyFormatters = { - AUSD: () => [], + Ausd: () => [], ForeignAsset: (value: TokensCurrencyId['asForeignAsset']) => [value.toString(10)], - Native: () => [], + Native: () => [''], Staking: () => ['BlockRewards'], Tranche: (value: TokensCurrencyId['asTranche']) => { return Array.isArray(value) diff --git a/src/mappings/services/epochService.ts b/src/mappings/services/epochService.ts index c1f23d48..31c3ebac 100644 --- a/src/mappings/services/epochService.ts +++ b/src/mappings/services/epochService.ts @@ -4,6 +4,7 @@ import { u64 } from '@polkadot/types' import { WAD } from '../../config' import { OrdersFulfillment } from '../../helpers/types' import { Epoch, EpochState } from '../../types' +import { assertPropInitialized } from '../../helpers/validation' export class EpochService extends Epoch { private states: EpochState[] @@ -106,9 +107,11 @@ export class EpochService extends Epoch { epochState.sumFulfilledRedeemOrders, epochState.tokenPrice ) + assertPropInitialized(this, 'sumInvestedAmount', 'bigint') + this.sumInvestedAmount! += epochState.sumFulfilledInvestOrders - this.sumInvestedAmount += epochState.sumFulfilledInvestOrders - this.sumRedeemedAmount += epochState.sumFulfilledRedeemOrdersCurrency + assertPropInitialized(this, 'sumRedeemedAmount', 'bigint') + this.sumRedeemedAmount! += epochState.sumFulfilledRedeemOrdersCurrency } return this } @@ -117,7 +120,8 @@ export class EpochService extends Epoch { logger.info(`Updating outstanding invest orders for epoch ${this.id}`) const trancheState = this.states.find((epochState) => epochState.trancheId === trancheId) if (trancheState === undefined) throw new Error(`No epochState with could be found for tranche: ${trancheId}`) - trancheState.sumOutstandingInvestOrders = trancheState.sumOutstandingInvestOrders + newAmount - oldAmount + assertPropInitialized(trancheState, 'sumOutstandingInvestOrders', 'bigint') + trancheState.sumOutstandingInvestOrders = trancheState.sumOutstandingInvestOrders! + newAmount - oldAmount return this } @@ -125,7 +129,8 @@ export class EpochService extends Epoch { logger.info(`Updating outstanding redeem orders for epoch ${this.id}`) const trancheState = this.states.find((trancheState) => trancheState.trancheId === trancheId) if (trancheState === undefined) throw new Error(`No epochState with could be found for tranche: ${trancheId}`) - trancheState.sumOutstandingRedeemOrders = trancheState.sumOutstandingRedeemOrders + newAmount - oldAmount + assertPropInitialized(trancheState, 'sumOutstandingRedeemOrders', 'bigint') + trancheState.sumOutstandingRedeemOrders = trancheState.sumOutstandingRedeemOrders! + newAmount - oldAmount trancheState.sumOutstandingRedeemOrdersCurrency = this.computeCurrencyAmount( trancheState.sumOutstandingRedeemOrders, tokenPrice @@ -139,17 +144,22 @@ export class EpochService extends Epoch { public increaseBorrowings(amount: bigint) { logger.info(`Increasing borrowings for epoch ${this.id} of ${amount}`) - this.sumBorrowedAmount += amount + assertPropInitialized(this, 'sumBorrowedAmount', 'bigint') + this.sumBorrowedAmount! += amount + return this } public increaseRepayments(amount: bigint) { logger.info(`Increasing repayments for epoch ${this.id} of ${amount}`) - this.sumRepaidAmount += amount + assertPropInitialized(this, 'sumRepaidAmount', 'bigint') + this.sumRepaidAmount! += amount + return this } public increasePaidFees(paidAmount: bigint) { logger.info(`Increasing paid fees for epoch ${this.id} by ${paidAmount.toString(10)}`) - this.sumPoolFeesPaidAmount += paidAmount + assertPropInitialized(this, 'sumPoolFeesPaidAmount', 'bigint') + this.sumPoolFeesPaidAmount! += paidAmount return this } } diff --git a/src/mappings/services/investorTransactionService.ts b/src/mappings/services/investorTransactionService.ts index a0015ec0..05636fd6 100644 --- a/src/mappings/services/investorTransactionService.ts +++ b/src/mappings/services/investorTransactionService.ts @@ -167,11 +167,11 @@ export class InvestorTransactionService extends InvestorTransaction { } static computeTokenAmount(data: InvestorTransactionData) { - return data.price ? nToBigInt(bnToBn(data.amount).mul(WAD).div(bnToBn(data.price))) : null + return data.price ? nToBigInt(bnToBn(data.amount).mul(WAD).div(bnToBn(data.price))) : undefined } static computeCurrencyAmount(data: InvestorTransactionData) { - return data.price ? nToBigInt(bnToBn(data.amount).mul(bnToBn(data.price)).div(WAD)) : null + return data.price ? nToBigInt(bnToBn(data.amount).mul(bnToBn(data.price)).div(WAD)) : undefined } static computeFulfilledAmount(data: InvestorTransactionData) { diff --git a/src/mappings/services/oracleTransactionService.ts b/src/mappings/services/oracleTransactionService.ts index 8a463693..53b4cd29 100644 --- a/src/mappings/services/oracleTransactionService.ts +++ b/src/mappings/services/oracleTransactionService.ts @@ -1,20 +1,22 @@ +import { assertPropInitialized } from '../../helpers/validation' import { OracleTransaction } from '../../types' -export interface OracleTransactionData { - readonly hash: string - readonly timestamp: Date - readonly key: string - readonly value?: bigint -} - export class OracleTransactionService extends OracleTransaction { - static init = (data: OracleTransactionData) => { - const tx = new this(`${data.hash}-${data.key.toString()}`, data.timestamp, data.key.toString(), data.value) - + static init(data: OracleTransactionData) { + const id = `${data.hash}-${data.key.toString()}` + logger.info(`Initialising new oracle transaction with id ${id} `) + assertPropInitialized(data, 'value', 'bigint') + const tx = new this(id, data.timestamp, data.key.toString(), data.value!) tx.timestamp = data.timestamp ?? null tx.key = data.key ?? null - tx.value = data.value ?? null - + tx.value = data.value! return tx } } + +export interface OracleTransactionData { + readonly hash: string + readonly timestamp: Date + readonly key: string + readonly value?: bigint +} diff --git a/src/mappings/services/outstandingOrderService.ts b/src/mappings/services/outstandingOrderService.ts index 105cda61..8d159da1 100644 --- a/src/mappings/services/outstandingOrderService.ts +++ b/src/mappings/services/outstandingOrderService.ts @@ -2,16 +2,18 @@ import { bnToBn, nToBigInt } from '@polkadot/util' import { paginatedGetter } from '../../helpers/paginatedGetter' import { OutstandingOrder } from '../../types' import { InvestorTransactionData } from './investorTransactionService' +import { assertPropInitialized } from '../../helpers/validation' export class OutstandingOrderService extends OutstandingOrder { static init(data: InvestorTransactionData, investAmount: bigint, redeemAmount: bigint) { + assertPropInitialized(data, 'epochNumber', 'number') const oo = new this( `${data.poolId}-${data.trancheId}-${data.address}`, data.hash, data.address, data.poolId, `${data.poolId}-${data.trancheId}`, - data.epochNumber, + data.epochNumber!, data.timestamp, investAmount, redeemAmount diff --git a/src/mappings/services/poolFeeService.ts b/src/mappings/services/poolFeeService.ts index ea71b9ac..a09ac3f1 100644 --- a/src/mappings/services/poolFeeService.ts +++ b/src/mappings/services/poolFeeService.ts @@ -1,3 +1,4 @@ +import { assertPropInitialized } from '../../helpers/validation' import { PoolFeeStatus, PoolFeeType } from '../../types' import { PoolFee } from '../../types/models' @@ -77,8 +78,13 @@ export class PoolFeeService extends PoolFee { public charge(data: Omit & Required>) { logger.info(`Charging PoolFee ${data.feeId} with amount ${data.amount.toString(10)}`) if (!this.isActive) throw new Error('Unable to charge inactive PolFee') - this.sumChargedAmount += data.amount - this.sumChargedAmountByPeriod += data.amount + + assertPropInitialized(this, 'sumChargedAmount', 'bigint') + this.sumChargedAmount! += data.amount + + assertPropInitialized(this, 'sumChargedAmountByPeriod', 'bigint') + this.sumChargedAmountByPeriod! += data.amount + this.pendingAmount = data.pending return this } @@ -86,8 +92,13 @@ export class PoolFeeService extends PoolFee { public uncharge(data: Omit & Required>) { logger.info(`Uncharging PoolFee ${data.feeId} with amount ${data.amount.toString(10)}`) if (!this.isActive) throw new Error('Unable to uncharge inactive PolFee') - this.sumChargedAmount -= data.amount - this.sumChargedAmountByPeriod -= data.amount + + assertPropInitialized(this, 'sumChargedAmount', 'bigint') + this.sumChargedAmount! -= data.amount + + assertPropInitialized(this, 'sumChargedAmountByPeriod', 'bigint') + this.sumChargedAmountByPeriod! -= data.amount + this.pendingAmount = data.pending return this } @@ -95,9 +106,15 @@ export class PoolFeeService extends PoolFee { public pay(data: Omit & Required>) { logger.info(`Paying PoolFee ${data.feeId} with amount ${data.amount.toString(10)}`) if (!this.isActive) throw new Error('Unable to pay inactive PolFee') - this.sumPaidAmount += data.amount - this.sumPaidAmountByPeriod += data.amount - this.pendingAmount -= data.amount + + assertPropInitialized(this, 'sumPaidAmount', 'bigint') + this.sumPaidAmount! += data.amount + + assertPropInitialized(this, 'sumPaidAmountByPeriod', 'bigint') + this.sumPaidAmountByPeriod! += data.amount + + assertPropInitialized(this, 'pendingAmount', 'bigint') + this.pendingAmount! -= data.amount return this } @@ -109,7 +126,9 @@ export class PoolFeeService extends PoolFee { this.pendingAmount = pending + disbursement const newAccruedAmount = this.pendingAmount - this.sumAccruedAmountByPeriod = newAccruedAmount - this.sumAccruedAmount + this.sumPaidAmountByPeriod + assertPropInitialized(this, 'sumAccruedAmount', 'bigint') + assertPropInitialized(this, 'sumPaidAmountByPeriod', 'bigint') + this.sumAccruedAmountByPeriod = newAccruedAmount - this.sumAccruedAmount! + this.sumPaidAmountByPeriod! this.sumAccruedAmount = newAccruedAmount return this } @@ -122,6 +141,9 @@ export class PoolFeeService extends PoolFee { static async computeSumPendingFees(poolId: string): Promise { logger.info(`Computing pendingFees for pool: ${poolId} `) const poolFees = await this.getByPoolId(poolId, { limit: 100 }) - return poolFees.reduce((sumPendingAmount, poolFee) => (sumPendingAmount + poolFee.pendingAmount), BigInt(0)) + return poolFees.reduce((sumPendingAmount, poolFee) => { + if (!poolFee.pendingAmount) throw new Error(`pendingAmount not available in poolFee ${poolFee.id}`) + return sumPendingAmount + poolFee.pendingAmount + }, BigInt(0)) } } diff --git a/src/mappings/services/poolService.test.ts b/src/mappings/services/poolService.test.ts index d879803a..a9d7b5bd 100644 --- a/src/mappings/services/poolService.test.ts +++ b/src/mappings/services/poolService.test.ts @@ -63,7 +63,7 @@ describe('Given a new pool, when initialised', () => { test('then reset accumulators are set to 0', () => { const resetAccumulators = Object.getOwnPropertyNames(pool).filter((prop) => prop.endsWith('ByPeriod')) for (const resetAccumulator of resetAccumulators) { - expect(pool[resetAccumulator]).toBe(BigInt(0)) + expect(pool[resetAccumulator as keyof typeof pool]).toBe(BigInt(0)) } }) diff --git a/src/mappings/services/poolService.ts b/src/mappings/services/poolService.ts index a8e222e1..9bb1f09c 100644 --- a/src/mappings/services/poolService.ts +++ b/src/mappings/services/poolService.ts @@ -15,6 +15,7 @@ import { cid, readIpfs } from '../../helpers/ipfsFetch' import { EpochService } from './epochService' import { WAD_DIGITS } from '../../config' import { CurrencyService } from './currencyService' +import { assertPropInitialized } from '../../helpers/validation' export class PoolService extends Pool { static seed(poolId: string, blockchain = '0') { @@ -140,7 +141,7 @@ export class PoolService extends Pool { if (poolReq.isNone) throw new Error('No pool data available to create the pool') const poolData = poolReq.unwrap() - this.metadata = metadataReq.isSome ? metadataReq.unwrap().metadata.toUtf8() : null + this.metadata = metadataReq.isSome ? metadataReq.unwrap().metadata.toUtf8() : undefined this.minEpochTime = poolData.parameters.minEpochTime.toNumber() this.maxPortfolioValuationAge = poolData.parameters.maxNavAge.toNumber() return this @@ -151,25 +152,26 @@ export class PoolService extends Pool { this.metadata = metadata } - public async initIpfsMetadata(): Promise { + public async initIpfsMetadata(): Promise['poolFees']> { if (!this.metadata) { logger.warn('No IPFS metadata') - return + return [] } - const metadata = await readIpfs(this.metadata.match(cid)[0]) + const matchedMetadata = this.metadata.match(cid) + if (!matchedMetadata || matchedMetadata.length !== 1) throw new Error('Unable to read metadata') + const metadata = await readIpfs(matchedMetadata[0]) this.name = metadata.pool.name this.assetClass = metadata.pool.asset.class this.assetSubclass = metadata.pool.asset.subClass this.icon = metadata.pool.icon.uri - return metadata.pool.poolFees ?? [] + return metadata.pool?.poolFees ?? [] } public async getIpfsPoolFeeMetadata(): Promise { if (!this.metadata) return logger.warn('No IPFS metadata') - const metadata = await readIpfs(this.metadata.match(cid)[0]) - if (!metadata.pool.poolFees) { - return null - } + const matchedMetadata = this.metadata.match(cid) + if (!matchedMetadata || matchedMetadata.length !== 1) throw new Error('Unable to read metadata') + const metadata = await readIpfs(matchedMetadata[0]) return metadata.pool.poolFees } @@ -178,9 +180,9 @@ export class PoolService extends Pool { const poolFeeMetadata = await this.getIpfsPoolFeeMetadata() if (!poolFeeMetadata) { logger.warn('Missing poolFee object in pool metadata!') - return null + return '' } - return poolFeeMetadata.find((elem) => elem.id.toString(10) === poolFeeId)?.name ?? null + return poolFeeMetadata.find((elem) => elem.id.toString(10) === poolFeeId)?.name ?? '' } static async getById(poolId: string) { @@ -230,15 +232,19 @@ export class PoolService extends Pool { private async updateNAVQuery() { logger.info(`Updating portfolio valuation for pool: ${this.id} (state)`) + assertPropInitialized(this, 'offchainCashValue', 'bigint') + assertPropInitialized(this, 'portfolioValuation', 'bigint') + assertPropInitialized(this, 'totalReserve', 'bigint') + const navResponse = await api.query.loans.portfolioValuation(this.id) - const newPortfolioValuation = navResponse.value.toBigInt() - this.offchainCashValue + const newPortfolioValuation = navResponse.value.toBigInt() - this.offchainCashValue! - this.deltaPortfolioValuationByPeriod = newPortfolioValuation - this.portfolioValuation + this.deltaPortfolioValuationByPeriod = newPortfolioValuation - this.portfolioValuation! this.portfolioValuation = newPortfolioValuation // The query was only used before fees were introduced, // so NAV == portfolioValuation + offchainCashValue + totalReserve - this.netAssetValue = newPortfolioValuation + this.offchainCashValue + this.totalReserve + this.netAssetValue = newPortfolioValuation + this.offchainCashValue! + this.totalReserve! logger.info( `portfolio valuation: ${this.portfolioValuation.toString(10)} delta: ${this.deltaPortfolioValuationByPeriod}` @@ -248,6 +254,10 @@ export class PoolService extends Pool { private async updateNAVCall() { logger.info(`Updating portfolio valuation for pool: ${this.id} (runtime)`) + + assertPropInitialized(this, 'offchainCashValue', 'bigint') + assertPropInitialized(this, 'portfolioValuation', 'bigint') + const apiCall = api.call as ExtendedCall const navResponse = await apiCall.poolsApi.nav(this.id) if (navResponse.isEmpty) { @@ -255,9 +265,9 @@ export class PoolService extends Pool { return } const newNAV = navResponse.unwrap().total.toBigInt() - const newPortfolioValuation = navResponse.unwrap().navAum.toBigInt() - this.offchainCashValue + const newPortfolioValuation = navResponse.unwrap().navAum.toBigInt() - this.offchainCashValue! - this.deltaPortfolioValuationByPeriod = newPortfolioValuation - this.portfolioValuation + this.deltaPortfolioValuationByPeriod = newPortfolioValuation - this.portfolioValuation! this.portfolioValuation = newPortfolioValuation this.netAssetValue = newNAV @@ -268,7 +278,10 @@ export class PoolService extends Pool { } public async updateNormalizedNAV() { - const currency = await CurrencyService.get(this.currencyId) + assertPropInitialized(this, 'currencyId', 'string') + assertPropInitialized(this, 'netAssetValue', 'bigint') + + const currency = await CurrencyService.get(this.currencyId!) if (!currency) throw new Error(`No currency with Id ${this.currencyId} found!`) const digitsMismatch = WAD_DIGITS - currency.decimals if (digitsMismatch === 0) { @@ -276,16 +289,18 @@ export class PoolService extends Pool { return this } if (digitsMismatch > 0) { - this.normalizedNAV = BigInt(this.netAssetValue.toString(10) + '0'.repeat(digitsMismatch)) + this.normalizedNAV = BigInt(this.netAssetValue!.toString(10) + '0'.repeat(digitsMismatch)) } else { - this.normalizedNAV = BigInt(this.netAssetValue.toString(10).substring(0, WAD_DIGITS)) + this.normalizedNAV = BigInt(this.netAssetValue!.toString(10).substring(0, WAD_DIGITS)) } return this } public increaseNumberOfAssets() { - this.sumNumberOfAssetsByPeriod += BigInt(1) - this.sumNumberOfAssets += BigInt(1) + assertPropInitialized(this, 'sumNumberOfAssetsByPeriod', 'bigint') + assertPropInitialized(this, 'sumNumberOfAssets', 'bigint') + this.sumNumberOfAssetsByPeriod! += BigInt(1) + this.sumNumberOfAssets! += BigInt(1) } public updateNumberOfActiveAssets(numberOfActiveAssets: bigint) { @@ -293,8 +308,10 @@ export class PoolService extends Pool { } public increaseBorrowings(borrowedAmount: bigint) { - this.sumBorrowedAmountByPeriod += borrowedAmount - this.sumBorrowedAmount += borrowedAmount + assertPropInitialized(this, 'sumBorrowedAmountByPeriod', 'bigint') + assertPropInitialized(this, 'sumBorrowedAmount', 'bigint') + this.sumBorrowedAmountByPeriod! += borrowedAmount + this.sumBorrowedAmount! += borrowedAmount } public increaseRepayments( @@ -302,27 +319,47 @@ export class PoolService extends Pool { interestRepaidAmount: bigint, unscheduledRepaidAmount: bigint ) { - this.sumRepaidAmountByPeriod += principalRepaidAmount + interestRepaidAmount + unscheduledRepaidAmount - this.sumRepaidAmount += principalRepaidAmount + interestRepaidAmount + unscheduledRepaidAmount - this.sumPrincipalRepaidAmountByPeriod += principalRepaidAmount - this.sumPrincipalRepaidAmount += principalRepaidAmount - this.sumInterestRepaidAmountByPeriod += interestRepaidAmount - this.sumInterestRepaidAmount += interestRepaidAmount - this.sumUnscheduledRepaidAmountByPeriod += unscheduledRepaidAmount - this.sumUnscheduledRepaidAmount += unscheduledRepaidAmount + assertPropInitialized(this, 'sumRepaidAmountByPeriod', 'bigint') + this.sumRepaidAmountByPeriod! += principalRepaidAmount + interestRepaidAmount + unscheduledRepaidAmount + + assertPropInitialized(this, 'sumRepaidAmount', 'bigint') + this.sumRepaidAmount! += principalRepaidAmount + interestRepaidAmount + unscheduledRepaidAmount + + assertPropInitialized(this, 'sumPrincipalRepaidAmountByPeriod', 'bigint') + this.sumPrincipalRepaidAmountByPeriod! += principalRepaidAmount + + assertPropInitialized(this, 'sumPrincipalRepaidAmount', 'bigint') + this.sumPrincipalRepaidAmount! += principalRepaidAmount + + assertPropInitialized(this, 'sumInterestRepaidAmountByPeriod', 'bigint') + this.sumInterestRepaidAmountByPeriod! += interestRepaidAmount + + assertPropInitialized(this, 'sumInterestRepaidAmount', 'bigint') + this.sumInterestRepaidAmount! += interestRepaidAmount + + assertPropInitialized(this, 'sumUnscheduledRepaidAmountByPeriod', 'bigint') + this.sumUnscheduledRepaidAmountByPeriod! += unscheduledRepaidAmount + + assertPropInitialized(this, 'sumUnscheduledRepaidAmount', 'bigint') + this.sumUnscheduledRepaidAmount! += unscheduledRepaidAmount } public increaseRepayments1024(repaidAmount: bigint) { - this.sumRepaidAmountByPeriod += repaidAmount - this.sumRepaidAmount += repaidAmount + assertPropInitialized(this, 'sumRepaidAmountByPeriod', 'bigint') + this.sumRepaidAmountByPeriod! += repaidAmount + + assertPropInitialized(this, 'sumRepaidAmount', 'bigint') + this.sumRepaidAmount! += repaidAmount } public increaseInvestments(currencyAmount: bigint) { - this.sumInvestedAmountByPeriod += currencyAmount + assertPropInitialized(this, 'sumInvestedAmountByPeriod', 'bigint') + this.sumInvestedAmountByPeriod! += currencyAmount } public increaseRedemptions(currencyAmount: bigint) { - this.sumRedeemedAmountByPeriod += currencyAmount + assertPropInitialized(this, 'sumRedeemedAmountByPeriod', 'bigint') + this.sumRedeemedAmountByPeriod! += currencyAmount } public closeEpoch(epochId: number) { @@ -341,17 +378,20 @@ export class PoolService extends Pool { public increaseDebtOverdue(amount: bigint) { logger.info(`Increasing sumDebtOverdue by ${amount}`) - this.sumDebtOverdue += amount + assertPropInitialized(this, 'sumDebtOverdue', 'bigint') + this.sumDebtOverdue! += amount } public increaseWriteOff(amount: bigint) { logger.info(`Increasing writeOff by ${amount}`) - this.sumDebtWrittenOffByPeriod += amount + assertPropInitialized(this, 'sumDebtWrittenOffByPeriod', 'bigint') + this.sumDebtWrittenOffByPeriod! += amount } public increaseInterestAccrued(amount: bigint) { logger.info(`Increasing interestAccrued by ${amount}`) - this.sumInterestAccruedByPeriod += amount + assertPropInitialized(this, 'sumInterestAccruedByPeriod', 'bigint') + this.sumInterestAccruedByPeriod! += amount } public async fetchTranchesFrom1400(): Promise { @@ -450,10 +490,10 @@ export class PoolService extends Pool { }, } = asset - const actualMaturityDate = maturity.isFixed ? new Date(maturity.asFixed.date.toNumber() * 1000) : null + const actualMaturityDate = maturity.isFixed ? new Date(maturity.asFixed.date.toNumber() * 1000) : undefined const timeToMaturity = actualMaturityDate ? Math.round((actualMaturityDate.valueOf() - Date.now().valueOf()) / 1000) - : null + : undefined obj[assetId.toString(10)] = { outstandingPrincipal: outstandingPrincipal.toBigInt(), @@ -481,10 +521,10 @@ export class PoolService extends Pool { let tokenPrices: Vec try { const apiRes = await (api.call as ExtendedCall).poolsApi.trancheTokenPrices(poolId) - tokenPrices = apiRes.isSome ? apiRes.unwrap() : undefined + tokenPrices = apiRes.unwrap() } catch (err) { logger.error(`Unable to fetch tranche token prices for pool: ${this.id}: ${err}`) - tokenPrices = undefined + return undefined } return tokenPrices } @@ -503,7 +543,13 @@ export class PoolService extends Pool { } logger.info(`Querying runtime poolFeesApi.listFees for pool ${this.id}`) const poolFeesListRequest = await apiCall.poolFeesApi.listFees(this.id) - const poolFeesList = poolFeesListRequest.unwrapOr([]) + let poolFeesList: PoolFeesList + try { + poolFeesList = poolFeesListRequest.unwrap() + } catch (error) { + console.error(error) + return [] + } const fees = poolFeesList.flatMap((poolFee) => poolFee.fees.filter((fee) => fee.amounts.feeType.isFixed)) const accruedFees = fees.map((fee): [feeId: string, pending: bigint, disbursement: bigint] => [ fee.id.toString(), @@ -515,29 +561,41 @@ export class PoolService extends Pool { public increaseChargedFees(chargedAmount: bigint) { logger.info(`Increasing charged fees for pool ${this.id} by ${chargedAmount.toString(10)}`) - this.sumPoolFeesChargedAmountByPeriod += chargedAmount - this.sumPoolFeesChargedAmount += chargedAmount + assertPropInitialized(this, 'sumPoolFeesChargedAmountByPeriod', 'bigint') + this.sumPoolFeesChargedAmountByPeriod! += chargedAmount + + assertPropInitialized(this, 'sumPoolFeesChargedAmount', 'bigint') + this.sumPoolFeesChargedAmount! += chargedAmount return this } public decreaseChargedFees(unchargedAmount: bigint) { logger.info(`Decreasing charged fees for pool ${this.id} by ${unchargedAmount.toString(10)}`) - this.sumPoolFeesChargedAmountByPeriod -= unchargedAmount - this.sumPoolFeesChargedAmount -= unchargedAmount + assertPropInitialized(this, 'sumPoolFeesChargedAmountByPeriod', 'bigint') + this.sumPoolFeesChargedAmountByPeriod! -= unchargedAmount + + assertPropInitialized(this, 'sumPoolFeesChargedAmount', 'bigint') + this.sumPoolFeesChargedAmount! -= unchargedAmount return this } public increaseAccruedFees(accruedAmount: bigint) { logger.info(`Increasing accrued fees for pool ${this.id} by ${accruedAmount.toString(10)}`) - this.sumPoolFeesAccruedAmountByPeriod += accruedAmount - this.sumPoolFeesAccruedAmount += accruedAmount + assertPropInitialized(this, 'sumPoolFeesAccruedAmountByPeriod', 'bigint') + this.sumPoolFeesAccruedAmountByPeriod! += accruedAmount + + assertPropInitialized(this, 'sumPoolFeesAccruedAmount', 'bigint') + this.sumPoolFeesAccruedAmount! += accruedAmount return this } public increasePaidFees(paidAmount: bigint) { logger.info(`Increasing paid fees for pool ${this.id} by ${paidAmount.toString(10)}`) - this.sumPoolFeesPaidAmountByPeriod += paidAmount - this.sumPoolFeesPaidAmount += paidAmount + assertPropInitialized(this, 'sumPoolFeesPaidAmountByPeriod', 'bigint') + this.sumPoolFeesPaidAmountByPeriod! += paidAmount + + assertPropInitialized(this, 'sumPoolFeesPaidAmount', 'bigint') + this.sumPoolFeesPaidAmount! += paidAmount return this } @@ -548,7 +606,8 @@ export class PoolService extends Pool { public increaseOffchainCashValue(amount: bigint) { logger.info(`Increasing offchainCashValue for pool ${this.id} by ${amount.toString(10)}`) - this.offchainCashValue += amount + assertPropInitialized(this, 'offchainCashValue', 'bigint') + this.offchainCashValue! += amount } public updateSumPoolFeesPendingAmount(pendingAmount: bigint) { @@ -558,7 +617,8 @@ export class PoolService extends Pool { public increaseRealizedProfitFifo(amount: bigint) { logger.info(`Increasing umRealizedProfitFifoByPeriod for pool ${this.id} by ${amount.toString(10)}`) - this.sumRealizedProfitFifoByPeriod += amount + assertPropInitialized(this, 'sumRealizedProfitFifoByPeriod', 'bigint') + this.sumRealizedProfitFifoByPeriod! += amount } public resetUnrealizedProfit() { @@ -568,11 +628,14 @@ export class PoolService extends Pool { this.sumUnrealizedProfitByPeriod = BigInt(0) } - public increaseUnrealizedProfit(atMarket: bigint, atNotional: bigint, byPeriod) { + public increaseUnrealizedProfit(atMarket: bigint, atNotional: bigint, byPeriod: bigint) { logger.info(`Increasing unrealizedProfit for pool ${this.id} atMarket: ${atMarket}, atNotional: ${atNotional}`) - this.sumUnrealizedProfitAtMarketPrice += atMarket - this.sumUnrealizedProfitAtNotional += atNotional - this.sumUnrealizedProfitByPeriod += byPeriod + assertPropInitialized(this, 'sumUnrealizedProfitAtMarketPrice', 'bigint') + assertPropInitialized(this, 'sumUnrealizedProfitAtNotional', 'bigint') + assertPropInitialized(this, 'sumUnrealizedProfitByPeriod', 'bigint') + this.sumUnrealizedProfitAtMarketPrice! += atMarket + this.sumUnrealizedProfitAtNotional! += atNotional + this.sumUnrealizedProfitByPeriod! += byPeriod } } @@ -582,9 +645,9 @@ export interface ActiveLoanData { outstandingInterest: bigint outstandingDebt: bigint presentValue: bigint - currentPrice: bigint - actualMaturityDate: Date - timeToMaturity: number + currentPrice: bigint | undefined + actualMaturityDate: Date | undefined + timeToMaturity: number | undefined actualOriginationDate: Date writeOffPercentage: bigint totalBorrowed: bigint diff --git a/src/mappings/services/trancheService.test.ts b/src/mappings/services/trancheService.test.ts index 67687a53..5d074054 100644 --- a/src/mappings/services/trancheService.test.ts +++ b/src/mappings/services/trancheService.test.ts @@ -56,8 +56,8 @@ describe('Given a new tranche, when initialised', () => { test('then reset accumulators are set to 0', () => { const resetAccumulators = Object.getOwnPropertyNames(tranches[0]).filter((prop) => prop.endsWith('ByPeriod')) for (const resetAccumulator of resetAccumulators) { - expect(tranches[0][resetAccumulator]).toBe(BigInt(0)) - expect(tranches[1][resetAccumulator]).toBe(BigInt(0)) + expect(tranches[0][resetAccumulator as keyof typeof tranches[0]]).toBe(BigInt(0)) + expect(tranches[1][resetAccumulator as keyof typeof tranches[1]]).toBe(BigInt(0)) } }) diff --git a/src/mappings/services/trancheService.ts b/src/mappings/services/trancheService.ts index 342bc4a8..80c876b4 100644 --- a/src/mappings/services/trancheService.ts +++ b/src/mappings/services/trancheService.ts @@ -63,16 +63,16 @@ export class TrancheService extends Tranche { } static async getByPoolId(poolId: string): Promise { - const tranches = await paginatedGetter(this, [['poolId', '=', poolId]]) - return tranches as TrancheService[] + const tranches = await paginatedGetter(this, [['poolId', '=', poolId]]) as TrancheService[] + return tranches } static async getActivesByPoolId(poolId: string): Promise { const tranches = await paginatedGetter(this, [ ['poolId', '=', poolId], ['isActive', '=', true], - ]) - return tranches as TrancheService[] + ]) as TrancheService[] + return tranches } public async updateSupply() { @@ -131,6 +131,7 @@ export class TrancheService extends Tranche { const apiCall = api.call as ExtendedCall const tokenPricesReq = await apiCall.poolsApi.trancheTokenPrices(poolId) if (tokenPricesReq.isNone) return this + if (typeof this.index !== 'number') throw new Error('Index is not a number') const tokenPrice = tokenPricesReq.unwrap()[this.index].toBigInt() logger.info(`Token price: ${tokenPrice.toString()}`) if (tokenPrice <= BigInt(0)) throw new Error(`Zero or negative price returned for tranche: ${this.id}`) @@ -150,7 +151,7 @@ export class TrancheService extends Tranche { `pool ${this.poolId} with reference date ${referencePeriodStart}` ) - let trancheSnapshot: TrancheSnapshot + let trancheSnapshot: TrancheSnapshot | undefined if (referencePeriodStart) { const trancheSnapshots = await TrancheSnapshot.getByPeriodId(referencePeriodStart.toISOString(), { limit: 100 }) if (trancheSnapshots.length === 0) { @@ -159,20 +160,20 @@ export class TrancheService extends Tranche { } trancheSnapshot = trancheSnapshots.find((snapshot) => snapshot.trancheId === `${this.poolId}-${this.trancheId}`) - if (trancheSnapshot === undefined) { + if (!trancheSnapshot) { logger.warn( `No tranche snapshot found tranche ${this.poolId}-${this.trancheId} with ` + `reference date ${referencePeriodStart}` ) return this } - if (typeof this.tokenPrice !== 'bigint') { + if (!this.tokenPrice) { logger.warn('Price information missing') return this } } const priceCurrent = bnToBn(this.tokenPrice) - const priceOld = referencePeriodStart ? bnToBn(trancheSnapshot.tokenPrice) : WAD + const priceOld = referencePeriodStart ? bnToBn(trancheSnapshot!.tokenPrice) : WAD this[yieldField] = nToBigInt(priceCurrent.mul(WAD).div(priceOld).sub(WAD)) logger.info(`Price: ${priceOld} to ${priceCurrent} = ${this[yieldField]}`) return this @@ -217,6 +218,8 @@ export class TrancheService extends Tranche { public updateOutstandingInvestOrders = (newAmount: bigint, oldAmount: bigint) => { logger.info(`Updating outstanding investment orders by period for tranche ${this.id}`) + if (typeof this.sumOutstandingInvestOrdersByPeriod !== 'bigint') + throw new Error('sumOutstandingInvestOrdersByPeriod not initialized') this.sumOutstandingInvestOrdersByPeriod = this.sumOutstandingInvestOrdersByPeriod + newAmount - oldAmount logger.info(`to ${this.sumOutstandingInvestOrdersByPeriod}`) return this @@ -224,6 +227,8 @@ export class TrancheService extends Tranche { public updateOutstandingRedeemOrders(newAmount: bigint, oldAmount: bigint) { logger.info(`Updating outstanding investment orders by period for tranche ${this.id}`) + if (typeof this.sumOutstandingRedeemOrdersByPeriod !== 'bigint') + throw new Error('sumOutstandingRedeemOrdersByPeriod not initialized') this.sumOutstandingRedeemOrdersByPeriod = this.sumOutstandingRedeemOrdersByPeriod + newAmount - oldAmount this.sumOutstandingRedeemOrdersCurrencyByPeriod = this.computeCurrencyAmount( this.sumOutstandingRedeemOrdersByPeriod @@ -234,6 +239,8 @@ export class TrancheService extends Tranche { public updateFulfilledInvestOrders(amount: bigint) { logger.info(`Updating fulfilled investment orders by period for tranche ${this.id}`) + if (typeof this.sumFulfilledInvestOrdersByPeriod !== 'bigint') + throw new Error('sumFulfilledInvestOrdersByPeriod not initialized') this.sumFulfilledInvestOrdersByPeriod = this.sumFulfilledInvestOrdersByPeriod + amount logger.info(`to ${this.sumFulfilledInvestOrdersByPeriod}`) return this @@ -241,6 +248,9 @@ export class TrancheService extends Tranche { public updateFulfilledRedeemOrders(amount: bigint) { logger.info(`Updating fulfilled redeem orders by period for tranche ${this.id}`) + if (typeof this.sumFulfilledRedeemOrdersByPeriod !== 'bigint') + throw new Error('sumFulfilledRedeemOrdersByPeriod not initialized') + this.sumFulfilledRedeemOrdersByPeriod = this.sumFulfilledRedeemOrdersByPeriod + amount this.sumFulfilledRedeemOrdersCurrencyByPeriod = this.computeCurrencyAmount(this.sumFulfilledRedeemOrdersByPeriod) logger.info(`to ${this.sumFulfilledRedeemOrdersByPeriod}`) @@ -277,4 +287,4 @@ export class TrancheService extends Tranche { } } -type BigIntFields = { [K in keyof T]: T[K] extends bigint ? K : never }[keyof T] +type BigIntFields = { [K in keyof Required]: Required[K] extends bigint ? K : never }[keyof Required] diff --git a/tsconfig.json b/tsconfig.json index 1873543c..0a06ee28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,15 +10,16 @@ "outDir": "dist", //"rootDir": "src", "target": "es2017", - //"strict": true + "strict": true }, "include": [ "src/**/*", "smoke-tests/*.ts", "smoke-tests/*.d.ts", "node_modules/@subql/types-core/dist/global.d.ts", - "node_modules/@subql/types/dist/global.d.ts" -, "smoke-tests/.test.ts" ], + "node_modules/@subql/types/dist/global.d.ts", + "smoke-tests/.test.ts" + ], "exclude": ["src/api-interfaces/**"], "exports": { "chaintypes": "./src/chaintypes.ts"