From 723e708055d775cfc08839274031fde2458a6384 Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Mon, 11 Nov 2024 20:36:42 +0100 Subject: [PATCH 1/6] 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" From c1619ebb663bb3fdf781c1e2dc95cc39346a292f Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Tue, 12 Nov 2024 11:05:51 +0100 Subject: [PATCH 2/6] fix: matching ipfs metadata string --- src/helpers/ipfsFetch.ts | 3 ++- src/mappings/services/poolService.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/helpers/ipfsFetch.ts b/src/helpers/ipfsFetch.ts index 0fd8cf15..66265bc5 100644 --- a/src/helpers/ipfsFetch.ts +++ b/src/helpers/ipfsFetch.ts @@ -1,7 +1,8 @@ import { IPFS_NODE } from '../config' export const cid = new RegExp( - '(Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})$' + '(Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})$', + 'g' ) export async function readIpfs>(ipfsId: string): Promise { diff --git a/src/mappings/services/poolService.ts b/src/mappings/services/poolService.ts index 9bb1f09c..3afab14e 100644 --- a/src/mappings/services/poolService.ts +++ b/src/mappings/services/poolService.ts @@ -167,12 +167,12 @@ export class PoolService extends Pool { return metadata.pool?.poolFees ?? [] } - public async getIpfsPoolFeeMetadata(): Promise { + public async getIpfsPoolFeeMetadata(): Promise['poolFees']> { if (!this.metadata) return logger.warn('No IPFS metadata') 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 + return metadata.pool.poolFees ?? [] } public async getIpfsPoolFeeName(poolFeeId: string): Promise { From 13d7a55d6e8c80a0e822294018dda12272d3f08d Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Tue, 12 Nov 2024 15:21:09 +0100 Subject: [PATCH 3/6] fix: improve handling of undefined get services --- src/@types/gobal.d.ts | 11 +- src/helpers/types.ts | 5 +- src/index.ts | 4 +- src/mappings/handlers/blockHandlers.ts | 5 +- src/mappings/handlers/ethHandlers.ts | 269 +++++++++--------- src/mappings/handlers/evmHandlers.ts | 6 +- src/mappings/handlers/investmentsHandlers.ts | 4 + src/mappings/handlers/loansHandlers.ts | 20 +- src/mappings/handlers/poolsHandlers.ts | 2 + src/mappings/services/accountService.test.ts | 5 +- src/mappings/services/assetCashflowService.ts | 9 +- src/mappings/services/assetService.test.ts | 7 +- src/mappings/services/assetService.ts | 13 +- .../services/currencyBalanceService.ts | 5 +- src/mappings/services/currencyService.test.ts | 4 +- src/mappings/services/currencyService.ts | 5 +- src/mappings/services/epochService.ts | 13 +- src/mappings/services/poolFeeService.ts | 4 +- src/mappings/services/poolService.test.ts | 14 +- src/mappings/services/poolService.ts | 29 +- src/mappings/services/trancheService.test.ts | 14 +- src/mappings/services/trancheService.ts | 15 +- tsconfig.json | 2 +- yarn.lock | 21 +- 24 files changed, 286 insertions(+), 200 deletions(-) diff --git a/src/@types/gobal.d.ts b/src/@types/gobal.d.ts index 2bfeafcc..a8b57a8d 100644 --- a/src/@types/gobal.d.ts +++ b/src/@types/gobal.d.ts @@ -1,4 +1,13 @@ -export {} +import { ApiPromise } from '@polkadot/api' +import type { Provider } from '@ethersproject/providers' +import { ApiDecoration } from '@polkadot/api/types' +import '@subql/types-core/dist/global' +export type ApiAt = ApiDecoration<'promise'> & { + rpc: ApiPromise['rpc'] +} declare global { + const api: ApiAt | Provider + const unsafeApi: ApiPromise | undefined function getNodeEvmChainId(): Promise } +export {} diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 10f3acbd..8292712f 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -3,6 +3,7 @@ import { AugmentedCall, AugmentedRpc, PromiseRpcResult } from '@polkadot/api/typ import { Enum, Null, Struct, u128, u32, u64, U8aFixed, Option, Vec, Bytes, Result, bool } from '@polkadot/types' import { AccountId32, Perquintill, Balance } from '@polkadot/types/interfaces' import { ITuple, Observable } from '@polkadot/types/types' +import type { ApiAt } from '../@types/gobal' export interface PoolDetails extends Struct { reserve: { total: u128; available: u128; max: u128 } @@ -503,7 +504,7 @@ export type PoolFeesList = Vec export type OracleFedEvent = ITuple<[feeder: DevelopmentRuntimeOriginCaller, key: OracleKey, value: u128]> -export type ExtendedRpc = typeof api.rpc & { +export type ExtendedRpc = ApiAt['rpc'] & { pools: { trancheTokenPrice: PromiseRpcResult< AugmentedRpc<(poolId: number | string, trancheId: number[]) => Observable> @@ -512,7 +513,7 @@ export type ExtendedRpc = typeof api.rpc & { } } -export type ExtendedCall = typeof api.call & { +export type ExtendedCall = ApiAt['call'] & { loansApi: { portfolio: AugmentedCall<'promise', (poolId: string) => Observable>>> expectedCashflows: AugmentedCall< diff --git a/src/index.ts b/src/index.ts index 761dea75..7acbe0fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,8 @@ import type { u64 } from '@polkadot/types' import type { Provider } from '@ethersproject/providers' const isSubstrateNode = 'query' in api -const isEvmNode = typeof (api as unknown as Provider).getNetwork === 'function' -const ethNetworkProm = isEvmNode ? (api as unknown as Provider).getNetwork() : null +const isEvmNode = typeof (api as Provider).getNetwork === 'function' +const ethNetworkProm = isEvmNode ? (api as Provider).getNetwork() : null global.fetch = fetch as unknown as typeof global.fetch global.atob = atob as typeof global.atob diff --git a/src/mappings/handlers/blockHandlers.ts b/src/mappings/handlers/blockHandlers.ts index e4d3fd92..146cbfc5 100644 --- a/src/mappings/handlers/blockHandlers.ts +++ b/src/mappings/handlers/blockHandlers.ts @@ -23,7 +23,9 @@ import { EpochService } from '../services/epochService' import { SnapshotPeriodService } from '../services/snapshotPeriodService' import { TrancheBalanceService } from '../services/trancheBalanceService' import { InvestorPositionService } from '../services/investorPositionService' +import { ApiAt } from '../../@types/gobal' +const cfgApi = api as ApiAt const timekeeper = TimekeeperService.init() export const handleBlock = errorHandler(_handleBlock) @@ -35,7 +37,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { const newPeriod = (await timekeeper).processBlock(block.timestamp) if (newPeriod) { - const specVersion = api.runtimeVersion.specVersion.toNumber() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() logger.info( `# It's a new period on block ${blockNumber}: ${block.timestamp.toISOString()} (specVersion: ${specVersion})` ) @@ -103,6 +105,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { pool.resetUnrealizedProfit() for (const loanId in activeLoanData) { const asset = await AssetService.getById(pool.id, loanId) + if(!asset) continue 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) diff --git a/src/mappings/handlers/ethHandlers.ts b/src/mappings/handlers/ethHandlers.ts index 5407e6c4..f74ce79e 100644 --- a/src/mappings/handlers/ethHandlers.ts +++ b/src/mappings/handlers/ethHandlers.ts @@ -33,131 +33,138 @@ async function _handleEthBlock(block: EthereumBlock): Promise { const newPeriod = (await timekeeper).processBlock(date) const blockPeriodStart = getPeriodStart(date) - if (newPeriod) { - logger.info(`It's a new period on EVM block ${blockNumber}: ${date.toISOString()}`) - const blockchain = await BlockchainService.getOrInit('1') - const currency = await CurrencyService.getOrInitEvm(blockchain.id, DAIMainnetAddress, DAISymbol, DAIName) - - const snapshotPeriod = SnapshotPeriodService.init(blockPeriodStart) - await snapshotPeriod.save() - - // update pool states - const poolUpdateCalls: PoolMulticall[] = [] + if (!newPeriod) return + logger.info(`It's a new period on EVM block ${blockNumber}: ${date.toISOString()}`) + const blockchain = await BlockchainService.getOrInit(chainId) + const currency = await CurrencyService.getOrInitEvm(blockchain.id, DAIMainnetAddress, DAISymbol, DAIName) + + const snapshotPeriod = SnapshotPeriodService.init(blockPeriodStart) + await snapshotPeriod.save() + + // update pool states + const processedPools: PoolService[] = [] + const poolUpdateCalls: PoolMulticall[] = [] + + for (const tinlakePool of tinlakePools) { + if (blockNumber < tinlakePool.startBlock) continue + const pool = await PoolService.getOrSeed(tinlakePool.id, false, false, blockchain.id) + processedPools.push(pool) + + const latestNavFeed = getLatestContract(tinlakePool.navFeed, blockNumber) + const latestReserve = getLatestContract(tinlakePool.reserve, blockNumber) + + // initialize new pool + if (!pool.isActive) { + await pool.initTinlake(tinlakePool.shortName, currency.id, date, blockNumber) + await pool.save() + + const senior = await TrancheService.getOrSeed(pool.id, 'senior', blockchain.id) + await senior.initTinlake(pool.id, `${pool.name} (Senior)`, 1, BigInt(tinlakePool.seniorInterestRate)) + await senior.save() + + const junior = await TrancheService.getOrSeed(pool.id, 'junior', blockchain.id) + await junior.initTinlake(pool.id, `${pool.name} (Junior)`, 0) + await junior.save() + } - for (const tinlakePool of tinlakePools) { - if (block.number >= tinlakePool.startBlock) { - const pool = await PoolService.getOrSeed(tinlakePool.id, false, false, blockchain.id) + //Append navFeed Call for pool + if (latestNavFeed && latestNavFeed.address) { + poolUpdateCalls.push({ + id: pool.id, + type: 'currentNAV', + call: { + target: latestNavFeed.address, + callData: NavfeedAbi__factory.createInterface().encodeFunctionData('currentNAV'), + }, + result: '', + }) + } + //Append totalBalance Call for pool + if (latestReserve && latestReserve.address) { + poolUpdateCalls.push({ + id: pool.id, + type: 'totalBalance', + call: { + target: latestReserve.address, + callData: ReserveAbi__factory.createInterface().encodeFunctionData('totalBalance'), + }, + result: '', + }) + } + } - // initialize new pool - if (!pool.isActive) { - await pool.initTinlake(tinlakePool.shortName, currency.id, date, blockNumber) - await pool.save() + //Execute available calls + const callResults: PoolMulticall[] = await processCalls(poolUpdateCalls).catch((err) => { + logger.error(`poolUpdateCalls failed: ${err}`) + return [] + }) - const senior = await TrancheService.getOrSeed(pool.id, 'senior', blockchain.id) - await senior.initTinlake(pool.id, `${pool.name} (Senior)`, 1, BigInt(tinlakePool.seniorInterestRate)) - await senior.save() + for (const callResult of callResults) { + // const tinlakePool = tinlakePools.find((p) => p.id === callResult.id) + // if (!tinlakePool) throw missingPool + const pool = processedPools.find( p => callResult.id === p.id) + if (!pool) throw missingPool - const junior = await TrancheService.getOrSeed(pool.id, 'junior', blockchain.id) - await junior.initTinlake(pool.id, `${pool.name} (Junior)`, 0) - await junior.save() - } + const tinlakePool = tinlakePools.find((p) => p.id === pool.id) + const latestNavFeed = getLatestContract(tinlakePool!.navFeed, blockNumber) + const latestReserve = getLatestContract(tinlakePool!.navFeed, blockNumber) - const latestNavFeed = getLatestContract(tinlakePool.navFeed, blockNumber) - const latestReserve = getLatestContract(tinlakePool.reserve, blockNumber) - - if (latestNavFeed && latestNavFeed.address) { - poolUpdateCalls.push({ - id: tinlakePool.id, - type: 'currentNAV', - call: { - target: latestNavFeed.address, - callData: NavfeedAbi__factory.createInterface().encodeFunctionData('currentNAV'), - }, - result: '', - }) - } - if (latestReserve && latestReserve.address) { - poolUpdateCalls.push({ - id: tinlakePool.id, - type: 'totalBalance', - call: { - target: latestReserve.address, - callData: ReserveAbi__factory.createInterface().encodeFunctionData('totalBalance'), - }, - result: '', - }) - } - } + // Update pool vurrentNav + if (callResult.type === 'currentNAV' && latestNavFeed) { + const currentNAV = + pool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK + ? BigInt(0) + : NavfeedAbi__factory.createInterface().decodeFunctionResult('currentNAV', callResult.result)[0].toBigInt() + pool.portfolioValuation = currentNAV + pool.netAssetValue = + pool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK + ? BigInt(0) + : (pool.portfolioValuation ?? BigInt(0)) + (pool.totalReserve ?? BigInt(0)) + await pool.updateNormalizedNAV() + await pool.save() + logger.info(`Updating pool ${pool.id} with portfolioValuation: ${pool.portfolioValuation}`) } - if (poolUpdateCalls.length > 0) { - const callResults = await processCalls(poolUpdateCalls) - for (const callResult of callResults) { - const tinlakePool = tinlakePools.find((p) => p.id === callResult.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) { - const currentNAV = - tinlakePool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK - ? BigInt(0) - : NavfeedAbi__factory.createInterface() - .decodeFunctionResult('currentNAV', callResult.result)[0] - .toBigInt() - pool.portfolioValuation = currentNAV - pool.netAssetValue = - tinlakePool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK - ? 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}`) - } - if (callResult.type === 'totalBalance' && latestReserve) { - const totalBalance = - tinlakePool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK - ? BigInt(0) - : ReserveAbi__factory.createInterface() - .decodeFunctionResult('totalBalance', callResult.result)[0] - .toBigInt() - pool.totalReserve = totalBalance - 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 && latestNavFeed.address && date.toDateString() === new Date().toDateString()) { - await updateLoans( - tinlakePool?.id as string, - date, - blockNumber, - tinlakePool?.shelf[0].address as string, - tinlakePool?.pile[0].address as string, - latestNavFeed.address - ) - } - } + // Update pool reserve + if (callResult.type === 'totalBalance' && latestReserve) { + const totalBalance = + pool.id === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK + ? BigInt(0) + : ReserveAbi__factory.createInterface().decodeFunctionResult('totalBalance', callResult.result)[0].toBigInt() + pool.totalReserve = totalBalance + pool.netAssetValue = (pool.portfolioValuation ?? BigInt(0)) + (pool.totalReserve ?? BigInt(0)) + await pool.updateNormalizedNAV() + await pool.save() + logger.info(`Updating pool ${pool.id} with totalReserve: ${pool.totalReserve}`) } - // Take snapshots - await evmStateSnapshotter( - 'periodId', - snapshotPeriod.id, - Pool, - PoolSnapshot, - block, - [['isActive', '=', true]], - 'poolId' - ) - //await evmStateSnapshotter('Asset', 'AssetSnapshot', block, 'isActive', true, 'assetId') - - //Update tracking of period and continue - await (await timekeeper).update(snapshotPeriod.start) + // Update loans (only index if fully synced) + if (latestNavFeed && latestNavFeed.address && date.toDateString() === new Date().toDateString()) { + await updateLoans( + pool.id, + date, + blockNumber, + tinlakePool!.shelf[0].address, + tinlakePool!.pile[0].address, + latestNavFeed.address + ) + } } + + // Take snapshots + await evmStateSnapshotter( + 'periodId', + snapshotPeriod.id, + Pool, + PoolSnapshot, + block, + [['isActive', '=', true]], + 'poolId' + ) + //await evmStateSnapshotter('Asset', 'AssetSnapshot', block, 'isActive', true, 'assetId') + + //Update tracking of period and continue + await (await timekeeper).update(snapshotPeriod.start) } type NewLoanData = { @@ -181,6 +188,7 @@ async function updateLoans( logger.info(`Found ${newLoans.length} new loans for pool ${poolId}`) const pool = await PoolService.getById(poolId) + if(!pool) throw missingPool const isAlt1AndAfterEndBlock = poolId === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK const nftIdCalls: PoolMulticall[] = [] @@ -197,7 +205,10 @@ async function updateLoans( } if (nftIdCalls.length > 0) { const newLoanData: NewLoanData[] = [] - const nftIdResponses = await processCalls(nftIdCalls) + const nftIdResponses: PoolMulticall[] = await processCalls(nftIdCalls).catch((err) => { + logger.error(`nftIdCalls failed: ${err}`) + return [] + }) for (const response of nftIdResponses) { if (response.result) { const data: NewLoanData = { @@ -229,7 +240,10 @@ async function updateLoans( result: '', }) } - const maturityDateResponses = await processCalls(maturityDateCalls) + const maturityDateResponses: PoolMulticall[] = await processCalls(maturityDateCalls).catch((err) => { + logger.error(`naturityDateCalls failed: ${err}`) + return [] + }) maturityDateResponses.map((response) => { if (response.result) { const loan = newLoanData.find((loan) => loan.id === response.id) @@ -305,7 +319,10 @@ async function updateLoans( }) }) if (loanDetailsCalls.length > 0) { - const loanDetailsResponses = await processCalls(loanDetailsCalls) + const loanDetailsResponses: PoolMulticall[] = await processCalls(loanDetailsCalls).catch((err) => { + logger.error(`loanDetailsCalls failed: ${err}`) + return [] + }) const loanDetails: LoanDetails = {} for (const loanDetailsResponse of loanDetailsResponses) { const loanId = loanDetailsResponse.id @@ -356,7 +373,7 @@ async function updateLoans( loan.outstandingDebt = debt const currentDebt = loan.outstandingDebt ?? BigInt(0) const rateGroup = loanDetails[loanIndex].loanRates - const pileContract = PileAbi__factory.connect(pile, api as unknown as Provider) + const pileContract = PileAbi__factory.connect(pile, api as Provider) if (!rateGroup) throw new Error(`Missing rateGroup for loan ${loan.id}`) const rates = await pileContract.rates(rateGroup) loan.interestRatePerSec = rates.ratePerSecond.toBigInt() @@ -398,7 +415,7 @@ async function updateLoans( async function getNewLoans(existingLoans: number[], shelfAddress: string) { let loanIndex = existingLoans.length || 1 const contractLoans: number[] = [] - const shelfContract = ShelfAbi__factory.connect(shelfAddress, api as unknown as Provider) + const shelfContract = ShelfAbi__factory.connect(shelfAddress, api as Provider) // eslint-disable-next-line while (true) { let response: Awaited> @@ -420,9 +437,7 @@ async function getNewLoans(existingLoans: number[], shelfAddress: string) { } function getLatestContract(contractArray: ContractArray[], blockNumber: number) { - return contractArray.reduce((prev, current: ContractArray) => - current.startBlock <= blockNumber && current.startBlock > (prev?.startBlock ?? 0) ? current : prev - ) + return contractArray.find((entry) => entry.startBlock <= blockNumber) } function chunkArray(array: T[], chunkSize: number): T[][] { @@ -434,11 +449,10 @@ function chunkArray(array: T[], chunkSize: number): T[][] { } async function processCalls(callsArray: PoolMulticall[], chunkSize = 30): Promise { + if (callsArray.length === 0) return [] const callChunks = chunkArray(callsArray, chunkSize) - for (let i = 0; i < callChunks.length; i++) { - const chunk = callChunks[i] - const multicall = MulticallAbi__factory.connect(multicallAddress, api as unknown as Provider) - // eslint-disable-next-line + for (const [i, chunk] of callChunks.entries()) { + const multicall = MulticallAbi__factory.connect(multicallAddress, api as Provider) let results: [BigNumber, string[]] & { blockNumber: BigNumber returnData: string[] @@ -452,7 +466,6 @@ async function processCalls(callsArray: PoolMulticall[], chunkSize = 30): Promis logger.error(`Error fetching chunk ${i}: ${e}`) } } - return callsArray } diff --git a/src/mappings/handlers/evmHandlers.ts b/src/mappings/handlers/evmHandlers.ts index 6ea4f1a6..9a7bf1a8 100644 --- a/src/mappings/handlers/evmHandlers.ts +++ b/src/mappings/handlers/evmHandlers.ts @@ -7,7 +7,7 @@ import { PoolService } from '../services/poolService' import { TrancheService } from '../services/trancheService' import { InvestorTransactionData, InvestorTransactionService } from '../services/investorTransactionService' import { CurrencyService } from '../services/currencyService' -import { BlockchainService, LOCAL_CHAIN_ID } from '../services/blockchainService' +import { BlockchainService } from '../services/blockchainService' import { CurrencyBalanceService } from '../services/currencyBalanceService' import type { Provider } from '@ethersproject/providers' import { TrancheBalanceService } from '../services/trancheBalanceService' @@ -15,7 +15,7 @@ import { escrows } from '../../config' import { InvestorPositionService } from '../services/investorPositionService' import { getPeriodStart } from '../../helpers/timekeeperService' -const _ethApi = api as unknown as Provider +const _ethApi = api as Provider //const networkPromise = typeof ethApi.getNetwork === 'function' ? ethApi.getNetwork() : null export const handleEvmDeployTranche = errorHandler(_handleEvmDeployTranche) @@ -24,7 +24,6 @@ async function _handleEvmDeployTranche(event: DeployTrancheLog): Promise { const [_poolId, _trancheId, tokenAddress] = event.args const poolManagerAddress = event.address - await BlockchainService.getOrInit(LOCAL_CHAIN_ID) const chainId = await getNodeEvmChainId() if (!chainId) throw new Error('Unable to retrieve chainId') const evmBlockchain = await BlockchainService.getOrInit(chainId) @@ -79,6 +78,7 @@ async function _handleEvmTransfer(event: TransferLog): Promise { 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) + if (!tranche) throw new Error('Tranche not found!') const orderData: Omit = { poolId: evmToken.poolId, diff --git a/src/mappings/handlers/investmentsHandlers.ts b/src/mappings/handlers/investmentsHandlers.ts index a8f61e9c..7e1a37bf 100644 --- a/src/mappings/handlers/investmentsHandlers.ts +++ b/src/mappings/handlers/investmentsHandlers.ts @@ -28,6 +28,7 @@ async function _handleInvestOrderUpdated(event: SubstrateEvent) { @@ -114,7 +117,7 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr 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 specVersion = cfgApi.runtimeVersion.specVersion.toNumber() const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -132,6 +135,8 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr // Update loan amount const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') + await asset.activate() const assetTransactionBaseData = { @@ -193,7 +198,7 @@ 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 specVersion = cfgApi.runtimeVersion.specVersion.toNumber() const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -211,6 +216,7 @@ async function _handleLoanRepaid(event: SubstrateEvent) { if (!epoch) throw new Error('Epoch not found!') const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') const assetTransactionBaseData = { poolId: poolId.toString(), assetId: loanId.toString(), @@ -280,6 +286,7 @@ async function _handleLoanWrittenOff(event: SubstrateEvent) logger.info(`Loan writtenoff event for pool: ${poolId.toString()} loanId: ${loanId.toString()}`) const { percentage, penalty } = status const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') await asset.writeOff(percentage.toBigInt(), penalty.toBigInt()) await asset.save() @@ -307,6 +314,7 @@ async function _handleLoanClosed(event: SubstrateEvent) { const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') await asset.close() await asset.save() @@ -330,7 +338,7 @@ async function _handleLoanClosed(event: SubstrateEvent) { export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred) async function _handleLoanDebtTransferred(event: SubstrateEvent) { - const specVersion = api.runtimeVersion.specVersion.toNumber() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() const [poolId, fromLoanId, toLoanId, _repaidAmount, _borrowAmount] = event.event.data const timestamp = event.block.timestamp @@ -356,7 +364,9 @@ async function _handleLoanDebtTransferred(event: SubstrateEvent const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') assertPropInitialized(pool, 'currentEpoch', 'number') const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) @@ -656,6 +669,7 @@ async function _handleLoanDebtDecreased(event: SubstrateEvent const account = await AccountService.getOrInit(event.extrinsic.extrinsic.signer.toHex()) const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') assertPropInitialized(pool, 'currentEpoch', 'number') const epoch = await EpochService.getById(pool.id, pool.currentEpoch!) diff --git a/src/mappings/handlers/poolsHandlers.ts b/src/mappings/handlers/poolsHandlers.ts index aef9d296..b65e5eea 100644 --- a/src/mappings/handlers/poolsHandlers.ts +++ b/src/mappings/handlers/poolsHandlers.ts @@ -55,6 +55,8 @@ async function _handlePoolCreated(event: SubstrateEvent): Prom for (const { id: feeId, name } of poolFeesMetadata) { const poolFee = await PoolFeeService.getById(pool.id, feeId.toString(10)) + if (!poolFee) throw new Error('poolFee not found!') + await poolFee.setName(name) await poolFee.save() } diff --git a/src/mappings/services/accountService.test.ts b/src/mappings/services/accountService.test.ts index ff240133..92f06bfd 100644 --- a/src/mappings/services/accountService.test.ts +++ b/src/mappings/services/accountService.test.ts @@ -1,8 +1,11 @@ +import { ApiAt } from '../../@types/gobal' import { AccountService } from './accountService' +const cfgApi = api as ApiAt + global.getNodeEvmChainId = () => Promise.resolve('2030') // eslint-disable-next-line @typescript-eslint/no-explicit-any -api.query['evmChainId'] = { chainId: jest.fn(() => ({ toString: () => '2030' })) } as any +cfgApi.query['evmChainId'] = { chainId: jest.fn(() => ({ toString: () => '2030' })) } as any test('Account is created in database', async () => { const id = 'ABCDE' diff --git a/src/mappings/services/assetCashflowService.ts b/src/mappings/services/assetCashflowService.ts index be284377..457987ca 100644 --- a/src/mappings/services/assetCashflowService.ts +++ b/src/mappings/services/assetCashflowService.ts @@ -1,6 +1,9 @@ +import type { ApiAt } from '../../@types/gobal' import { ExtendedCall } from '../../helpers/types' import { AssetCashflow } from '../../types/models/AssetCashflow' +const cfgApi = api as ApiAt + export class AssetCashflowService extends AssetCashflow { static init(assetId: string, timestamp: Date, principal: bigint, interest: bigint) { const id = `${assetId}-${timestamp.valueOf()}` @@ -9,12 +12,12 @@ export class AssetCashflowService extends AssetCashflow { } static async recordAssetCashflows(_assetId: string) { - const specVersion = api.runtimeVersion.specVersion.toNumber() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() if (specVersion < 1103) return const [poolId, assetId] = _assetId.split('-') logger.info(`Recording AssetCashflows for Asset ${_assetId}`) - const apiCall = api.call as ExtendedCall - logger.info(`Calling runtime API loansApi.expectedCashflows(${poolId}, ${assetId})`) + const apiCall = cfgApi.call as ExtendedCall + logger.info(`Calling runtime API loanscfgApi.expectedCashflows(${poolId}, ${assetId})`) const response = await apiCall.loansApi.expectedCashflows(poolId, assetId) logger.info(JSON.stringify(response)) if(!response.isOk) return diff --git a/src/mappings/services/assetService.test.ts b/src/mappings/services/assetService.test.ts index ab8b8356..4d72ca77 100644 --- a/src/mappings/services/assetService.test.ts +++ b/src/mappings/services/assetService.test.ts @@ -1,6 +1,9 @@ +import { ApiAt } from '../../@types/gobal' import { AssetType, AssetValuationMethod } from '../../types' import { AssetService } from './assetService' +const cfgApi = api as ApiAt + const poolId = '1111111111' const loanId = 'ABCD' const nftClassId = BigInt(1) @@ -8,7 +11,7 @@ const nftItemId = BigInt(2) const timestamp = new Date() const metadata = 'AAAAAA' -api.query['uniques'] = { +cfgApi.query['uniques'] = { instanceMetadataOf: jest.fn(() => ({ isNone: false, unwrap: () => ({ data: { toUtf8: () => metadata } }), @@ -41,7 +44,7 @@ describe('Given a new loan, when initialised', () => { test('when the metadata is fetched, then the correct values are set', async () => { await loan.updateItemMetadata() - expect(api.query.uniques.instanceMetadataOf).toHaveBeenCalledWith(nftClassId, nftItemId) + expect(cfgApi.query.uniques.instanceMetadataOf).toHaveBeenCalledWith(nftClassId, nftItemId) expect(loan.metadata).toBe(metadata) }) diff --git a/src/mappings/services/assetService.ts b/src/mappings/services/assetService.ts index 720c618e..9e388c56 100644 --- a/src/mappings/services/assetService.ts +++ b/src/mappings/services/assetService.ts @@ -6,6 +6,9 @@ import { Asset, AssetType, AssetValuationMethod, AssetStatus, AssetSnapshot } fr import { ActiveLoanData } from './poolService' import { cid, readIpfs } from '../../helpers/ipfsFetch' import { assertPropInitialized } from '../../helpers/validation' +import { ApiAt } from '../../@types/gobal' + +const cfgApi = api as ApiAt export const ONCHAIN_CASH_ASSET_ID = '0' export class AssetService extends Asset { @@ -81,7 +84,7 @@ export class AssetService extends Asset { } static async getById(poolId: string, assetId: string) { - const asset = (await this.get(`${poolId}-${assetId}`)) as AssetService + const asset = (await this.get(`${poolId}-${assetId}`)) as AssetService | undefined return asset } @@ -94,7 +97,7 @@ export class AssetService extends Asset { ], { limit: 100 } ) - ).pop() as AssetService + ).pop() as AssetService | undefined return asset } @@ -157,7 +160,7 @@ export class AssetService extends Asset { public async updateActiveAssetData(activeAssetData: ActiveLoanData[keyof ActiveLoanData]) { // Current price was always 0 until spec version 1025 - const specVersion = api.runtimeVersion.specVersion.toNumber() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() if (specVersion < 1025) delete activeAssetData.currentPrice // Set all active asset values @@ -184,7 +187,7 @@ export class AssetService extends Asset { `collectionId ${this.collateralNftClassId!.toString()}, ` + `itemId: ${this.collateralNftItemId!.toString()}` ) - const itemMetadata = await api.query.uniques.instanceMetadataOf>( + const itemMetadata = await cfgApi.query.uniques.instanceMetadataOf>( this.collateralNftClassId, this.collateralNftItemId ) @@ -250,7 +253,7 @@ export class AssetService extends Asset { public async updateExternalAssetPricingFromState() { logger.info(`Executing state call loans.activeLoans to update asset ${this.id} pricing information`) - const loansCall = await api.query.loans.activeLoans(this.poolId) + const loansCall = await cfgApi.query.loans.activeLoans(this.poolId) const assetTuple = loansCall.find((tuple) => tuple[0].toString(10) === this.id.split('-')[1]) if (!assetTuple) throw new Error(`Asset ${this.id} not found in pool active loans!`) const loanData = assetTuple[1] diff --git a/src/mappings/services/currencyBalanceService.ts b/src/mappings/services/currencyBalanceService.ts index 89439814..332a8625 100644 --- a/src/mappings/services/currencyBalanceService.ts +++ b/src/mappings/services/currencyBalanceService.ts @@ -1,7 +1,10 @@ +import { ApiAt } from '../../@types/gobal' import { AccountData } from '../../helpers/types' import { CurrencyBalance } from '../../types/models/CurrencyBalance' import { formatEnumPayload } from './currencyService' +const cfgApi = api as ApiAt + export class CurrencyBalanceService extends CurrencyBalance { static init(address: string, currency: string) { logger.info(`Initialising new CurrencyBalance: ${address}-${currency} to 0`) @@ -27,7 +30,7 @@ export class CurrencyBalanceService extends CurrencyBalance { public async getBalance() { const [_chainId, currencyType, ...currencySpec] = this.currencyId.split('-') const enumPayload = formatEnumPayload(currencyType, ...currencySpec) - const balanceResponse = await api.query.ormlTokens.accounts(this.accountId, enumPayload) + const balanceResponse = await cfgApi.query.ormlTokens.accounts(this.accountId, enumPayload) this.amount = balanceResponse.free.toBigInt() logger.info(`Fetched initial balance of for CurrencyBalance ${this.id} of ${this.amount.toString(10)}`) } diff --git a/src/mappings/services/currencyService.test.ts b/src/mappings/services/currencyService.test.ts index 21d0f066..be815990 100644 --- a/src/mappings/services/currencyService.test.ts +++ b/src/mappings/services/currencyService.test.ts @@ -1,9 +1,11 @@ +import { ApiAt } from '../../@types/gobal' import { CurrencyService } from './currencyService' +const cfgApi = api as ApiAt const entityName = 'Currency' // eslint-disable-next-line @typescript-eslint/no-explicit-any -api.query['ormlAssetRegistry'] = { metadata: jest.fn(() => ({ isSome: false })) } as any +cfgApi.query['ormlAssetRegistry'] = { metadata: jest.fn(() => ({ isSome: false })) } as any const stdDecimals = 18 diff --git a/src/mappings/services/currencyService.ts b/src/mappings/services/currencyService.ts index d58bd645..7a4bbc69 100644 --- a/src/mappings/services/currencyService.ts +++ b/src/mappings/services/currencyService.ts @@ -3,6 +3,9 @@ import { AssetMetadata } from '@polkadot/types/interfaces' import { Currency } from '../../types/models/Currency' import { WAD_DIGITS } from '../../config' import type { TokensCurrencyId } from '../../helpers/types' +import { ApiAt } from '../../@types/gobal' + +const cfgApi = api as ApiAt export class CurrencyService extends Currency { static init(chainId: string, currencyId: string, decimals: number, symbol?: string, name?: string) { @@ -20,7 +23,7 @@ export class CurrencyService extends Currency { let currency: CurrencyService = (await this.get(id)) as CurrencyService if (!currency) { const enumPayload = formatEnumPayload(currencyType, ...currencyValue) - const assetMetadata = (await api.query.ormlAssetRegistry.metadata(enumPayload)) as Option + const assetMetadata = (await cfgApi.query.ormlAssetRegistry.metadata(enumPayload)) as Option const decimals = assetMetadata.isSome ? assetMetadata.unwrap().decimals.toNumber() : WAD_DIGITS const symbol = assetMetadata.isSome ? assetMetadata.unwrap().symbol.toUtf8() : undefined const name = assetMetadata.isSome ? assetMetadata.unwrap().name.toUtf8() : undefined diff --git a/src/mappings/services/epochService.ts b/src/mappings/services/epochService.ts index 31c3ebac..7b499b8e 100644 --- a/src/mappings/services/epochService.ts +++ b/src/mappings/services/epochService.ts @@ -5,6 +5,9 @@ import { WAD } from '../../config' import { OrdersFulfillment } from '../../helpers/types' import { Epoch, EpochState } from '../../types' import { assertPropInitialized } from '../../helpers/validation' +import { ApiAt } from '../../@types/gobal' + +const cfgApi = api as ApiAt export class EpochService extends Epoch { private states: EpochState[] @@ -73,16 +76,16 @@ export class EpochService extends Epoch { logger.info(`Fetching data for tranche: ${epochState.trancheId}`) const trancheCurrency = [this.poolId, epochState.trancheId] const [investOrderId, redeemOrderId] = await Promise.all([ - api.query.investments.investOrderId(trancheCurrency), - api.query.investments.redeemOrderId(trancheCurrency), + cfgApi.query.investments.investOrderId(trancheCurrency), + cfgApi.query.investments.redeemOrderId(trancheCurrency), ]) logger.info(`investOrderId: ${investOrderId.toNumber()}, redeemOrderId: ${redeemOrderId.toNumber()}`) const [investOrderFulfillment, redeemOrderFulfillment] = await Promise.all([ - api.query.investments.clearedInvestOrders>( + cfgApi.query.investments.clearedInvestOrders>( trancheCurrency, investOrderId.toNumber() - 1 ), - api.query.investments.clearedRedeemOrders>( + cfgApi.query.investments.clearedRedeemOrders>( trancheCurrency, redeemOrderId.toNumber() - 1 ), @@ -105,7 +108,7 @@ export class EpochService extends Epoch { ) epochState.sumFulfilledRedeemOrdersCurrency = this.computeCurrencyAmount( epochState.sumFulfilledRedeemOrders, - epochState.tokenPrice + epochState.tokenPrice! ) assertPropInitialized(this, 'sumInvestedAmount', 'bigint') this.sumInvestedAmount! += epochState.sumFulfilledInvestOrders diff --git a/src/mappings/services/poolFeeService.ts b/src/mappings/services/poolFeeService.ts index a09ac3f1..0fbfe8f0 100644 --- a/src/mappings/services/poolFeeService.ts +++ b/src/mappings/services/poolFeeService.ts @@ -40,7 +40,7 @@ export class PoolFeeService extends PoolFee { blockchain = '0' ) { const { poolId, feeId } = data - let poolFee = (await this.get(`${poolId}-${feeId}`)) as PoolFeeService + let poolFee = (await this.get(`${poolId}-${feeId}`)) as PoolFeeService | undefined if (!poolFee) { poolFee = this.init(data, type, status, blockchain) } else { @@ -50,7 +50,7 @@ export class PoolFeeService extends PoolFee { } static getById(poolId: string, feeId: string) { - return this.get(`${poolId}-${feeId}`) as Promise + return this.get(`${poolId}-${feeId}`) as Promise } static async propose(data: PoolFeeData, type: keyof typeof PoolFeeType) { diff --git a/src/mappings/services/poolService.test.ts b/src/mappings/services/poolService.test.ts index a9d7b5bd..1beb35f2 100644 --- a/src/mappings/services/poolService.test.ts +++ b/src/mappings/services/poolService.test.ts @@ -1,6 +1,8 @@ +import { ApiAt } from '../../@types/gobal' import { PoolService } from './poolService' +const cfgApi = api as ApiAt -api.query['poolSystem'] = { +cfgApi.query['poolSystem'] = { pool: jest.fn(() => ({ isSome: true, isNone: false, @@ -21,7 +23,7 @@ api.query['poolSystem'] = { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any -api.query['poolRegistry'] = { +cfgApi.query['poolRegistry'] = { poolMetadata: jest.fn(() => ({ isSome: true, isNone: false, @@ -30,7 +32,7 @@ api.query['poolRegistry'] = { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any -api.query['loans'] = { +cfgApi.query['loans'] = { portfolioValuation: jest.fn(() => ({ value: { toBigInt: () => BigInt(100000000000000), @@ -69,7 +71,7 @@ describe('Given a new pool, when initialised', () => { test('when the pool data is initialised, then the correct values are fetched and set', async () => { await pool.initData() - expect(api.query.poolSystem.pool).toBeCalledWith(poolId) + expect(cfgApi.query.poolSystem.pool).toBeCalledWith(poolId) expect(pool).toMatchObject({ currencyId: 'AUSD', metadata: 'AAA', @@ -87,13 +89,13 @@ describe('Given a new pool, when initialised', () => { describe('Given an existing pool,', () => { test.skip('when the nav is updated, then the value is fetched and set correctly', async () => { await pool.updateNAV() - expect(api.query.loans.portfolioValuation).toHaveBeenCalled() + expect(cfgApi.query.loans.portfolioValuation).toHaveBeenCalled() expect(pool.portfolioValuation).toBe(BigInt(100000000000000)) }) test('when the pool state is updated, then the values are fetched and set correctly', async () => { await pool.updateState() - expect(api.query.poolSystem.pool).toHaveBeenCalledWith(poolId) + expect(cfgApi.query.poolSystem.pool).toHaveBeenCalledWith(poolId) expect(pool).toMatchObject({ totalReserve: BigInt(91000000000000), availableReserve: BigInt(92000000000000), diff --git a/src/mappings/services/poolService.ts b/src/mappings/services/poolService.ts index 3afab14e..72707239 100644 --- a/src/mappings/services/poolService.ts +++ b/src/mappings/services/poolService.ts @@ -16,6 +16,9 @@ import { EpochService } from './epochService' import { WAD_DIGITS } from '../../config' import { CurrencyService } from './currencyService' import { assertPropInitialized } from '../../helpers/validation' +import { ApiAt } from '../../@types/gobal' + +const cfgApi = api as ApiAt export class PoolService extends Pool { static seed(poolId: string, blockchain = '0') { @@ -134,8 +137,8 @@ export class PoolService extends Pool { public async initData() { logger.info(`Initialising data for pool: ${this.id}`) const [poolReq, metadataReq] = await Promise.all([ - api.query.poolSystem.pool>(this.id), - api.query.poolRegistry.poolMetadata>(this.id), + cfgApi.query.poolSystem.pool>(this.id), + cfgApi.query.poolRegistry.poolMetadata>(this.id), ]) if (poolReq.isNone) throw new Error('No pool data available to create the pool') @@ -186,7 +189,7 @@ export class PoolService extends Pool { } static async getById(poolId: string) { - return this.get(poolId) as Promise + return this.get(poolId) as Promise } static async getAll() { @@ -204,7 +207,7 @@ export class PoolService extends Pool { } public async updateState() { - const poolResponse = await api.query.poolSystem.pool>(this.id) + const poolResponse = await cfgApi.query.poolSystem.pool>(this.id) logger.info(`Updating state for pool: ${this.id}`) if (poolResponse.isSome) { const poolData = poolResponse.unwrap() @@ -216,8 +219,8 @@ export class PoolService extends Pool { } public async updateNAV() { - const specVersion = api.runtimeVersion.specVersion.toNumber() - const specName = api.runtimeVersion.specName.toString() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specName = cfgApi.runtimeVersion.specName.toString() switch (specName) { case 'centrifuge-devel': await (specVersion < 1038 ? this.updateNAVQuery() : this.updateNAVCall()) @@ -236,7 +239,7 @@ export class PoolService extends Pool { assertPropInitialized(this, 'portfolioValuation', 'bigint') assertPropInitialized(this, 'totalReserve', 'bigint') - const navResponse = await api.query.loans.portfolioValuation(this.id) + const navResponse = await cfgApi.query.loans.portfolioValuation(this.id) const newPortfolioValuation = navResponse.value.toBigInt() - this.offchainCashValue! this.deltaPortfolioValuationByPeriod = newPortfolioValuation - this.portfolioValuation! @@ -396,7 +399,7 @@ export class PoolService extends Pool { public async fetchTranchesFrom1400(): Promise { logger.info(`Fetching tranches for pool: ${this.id} with specVersion >= 1400`) - const poolResponse = await api.query.poolSystem.pool>(this.id) + const poolResponse = await cfgApi.query.poolSystem.pool>(this.id) if (poolResponse.isNone) throw new Error('Unable to fetch pool data!') const poolData = poolResponse.unwrap() const { ids, tranches } = poolData.tranches @@ -428,7 +431,7 @@ export class PoolService extends Pool { public async fetchTranchesBefore1400(): Promise { logger.info(`Fetching tranches for pool: ${this.id} with specVersion < 1400`) - const poolResponse = await api.query.poolSystem.pool>(this.id) + const poolResponse = await cfgApi.query.poolSystem.pool>(this.id) if (poolResponse.isNone) throw new Error('Unable to fetch pool data!') const poolData = poolResponse.unwrap() const { ids, tranches } = poolData.tranches @@ -459,7 +462,7 @@ export class PoolService extends Pool { } public async getTranches() { - const specVersion = api.runtimeVersion.specVersion.toNumber() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() let tranches: TrancheData[] if (specVersion >= 1400) { tranches = await this.fetchTranchesFrom1400() @@ -530,9 +533,9 @@ export class PoolService extends Pool { } public async getAccruedFees() { - const apiCall = api.call as ExtendedCall - const specVersion = api.runtimeVersion.specVersion.toNumber() - const specName = api.runtimeVersion.specName.toString() + const apiCall = cfgApi.call as ExtendedCall + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specName = cfgApi.runtimeVersion.specName.toString() switch (specName) { case 'centrifuge-devel': if (specVersion < 1040) return [] diff --git a/src/mappings/services/trancheService.test.ts b/src/mappings/services/trancheService.test.ts index 5d074054..86818f7a 100644 --- a/src/mappings/services/trancheService.test.ts +++ b/src/mappings/services/trancheService.test.ts @@ -1,16 +1,18 @@ +import { ApiAt } from '../../@types/gobal' import { errorLogger } from '../../helpers/errorHandler' import { ExtendedCall } from '../../helpers/types' import { TrancheService } from './trancheService' +const cfgApi = api as ApiAt -api.query['ormlTokens'] = { +cfgApi.query['ormlTokens'] = { totalIssuance: jest.fn(() => ({ toBigInt: () => BigInt('9999000000000000000000') })), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any // eslint-disable-next-line @typescript-eslint/no-explicit-any -api['runtimeVersion'] = { specVersion: { toNumber: ()=> 1029 } } as any +cfgApi['runtimeVersion'] = { specVersion: { toNumber: () => 1029 } } as any -api.call['poolsApi'] = { +cfgApi.call['poolsApi'] = { trancheTokenPrices: jest.fn(() => ({ isSome: true, isNone: false, @@ -56,14 +58,14 @@ 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 as keyof typeof tranches[0]]).toBe(BigInt(0)) - expect(tranches[1][resetAccumulator as keyof typeof tranches[1]]).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)) } }) test('when the supply data is fetched, then the correct values are fetched and set', async () => { await tranches[0].updateSupply() - expect(api.query.ormlTokens.totalIssuance).toHaveBeenCalledWith({ Tranche: [poolId, trancheIds[0]] }) + expect(cfgApi.query.ormlTokens.totalIssuance).toHaveBeenCalledWith({ Tranche: [poolId, trancheIds[0]] }) expect(tranches[0]).toMatchObject({ tokenSupply: BigInt('9999000000000000000000') }) }) diff --git a/src/mappings/services/trancheService.ts b/src/mappings/services/trancheService.ts index 80c876b4..1fcdcf8a 100644 --- a/src/mappings/services/trancheService.ts +++ b/src/mappings/services/trancheService.ts @@ -5,6 +5,9 @@ import { WAD } from '../../config' import { ExtendedCall } from '../../helpers/types' import { Tranche, TrancheSnapshot } from '../../types' import { TrancheData } from './poolService' +import { ApiAt } from '../../@types/gobal' + +const cfgApi = api as ApiAt const MAINNET_CHAINID = '0xb3db41421702df9a7fcac62b53ffeac85f7853cc4e689e0b93aeb3db18c09d82' @@ -58,33 +61,33 @@ export class TrancheService extends Tranche { } static async getById(poolId: string, trancheId: string) { - const tranche = (await this.get(`${poolId}-${trancheId}`)) as TrancheService + const tranche = (await this.get(`${poolId}-${trancheId}`)) as TrancheService | undefined return tranche } static async getByPoolId(poolId: string): Promise { - const tranches = await paginatedGetter(this, [['poolId', '=', poolId]]) as TrancheService[] + const tranches = (await paginatedGetter(this, [['poolId', '=', poolId]])) as TrancheService[] return tranches } static async getActivesByPoolId(poolId: string): Promise { - const tranches = await paginatedGetter(this, [ + const tranches = (await paginatedGetter(this, [ ['poolId', '=', poolId], ['isActive', '=', true], - ]) as TrancheService[] + ])) as TrancheService[] return tranches } public async updateSupply() { logger.info(`Updating supply for tranche ${this.id}`) const requestPayload = { Tranche: [this.poolId, this.trancheId] } - const supplyResponse = await api.query.ormlTokens.totalIssuance(requestPayload) + const supplyResponse = await cfgApi.query.ormlTokens.totalIssuance(requestPayload) this.tokenSupply = supplyResponse.toBigInt() return this } public async updatePrice(price: bigint, block?: number) { - const specVersion = api.runtimeVersion.specVersion.toNumber() + const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() if (MAINNET_CHAINID === chainId && !!block) { if (block < 4058350) return this.updatePriceFixDecimalError(price, block) if (specVersion >= 1025 && specVersion < 1029) return await this.updatePriceFixForFees(price) diff --git a/tsconfig.json b/tsconfig.json index 0a06ee28..1c25e74a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "smoke-tests/*.ts", "smoke-tests/*.d.ts", "node_modules/@subql/types-core/dist/global.d.ts", - "node_modules/@subql/types/dist/global.d.ts", + //"node_modules/@subql/types/dist/global.d.ts", "smoke-tests/.test.ts" ], "exclude": ["src/api-interfaces/**"], diff --git a/yarn.lock b/yarn.lock index 206d327c..3500994c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2454,17 +2454,17 @@ "@types/node" "*" "@types/node-fetch@^2.6.11": - version "2.6.11" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" - integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== dependencies: "@types/node" "*" form-data "^4.0.0" -"@types/node@*", "@types/node@^22.5.5": - version "22.8.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.4.tgz#ab754f7ac52e1fe74174f761c5b03acaf06da0dc" - integrity sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw== +"@types/node@*": + version "22.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" + integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== dependencies: undici-types "~6.19.8" @@ -2480,6 +2480,13 @@ dependencies: undici-types "~6.19.2" +"@types/node@^22.5.5": + version "22.8.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.4.tgz#ab754f7ac52e1fe74174f761c5b03acaf06da0dc" + integrity sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw== + dependencies: + undici-types "~6.19.8" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" From b3302a7d73a0bf5d7d1fe71c4d678eed96ebe229 Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Tue, 12 Nov 2024 23:24:42 +0100 Subject: [PATCH 4/6] fix: enhanced in memory snapshotting --- src/@types/gobal.d.ts | 6 +- src/helpers/stateSnapshot.ts | 48 +++++++++++-- src/helpers/types.ts | 5 +- src/mappings/handlers/blockHandlers.ts | 70 +++++-------------- src/mappings/handlers/ethHandlers.ts | 46 +++++------- src/mappings/handlers/loansHandlers.ts | 9 +-- src/mappings/handlers/poolsHandlers.ts | 13 ++-- src/mappings/services/accountService.test.ts | 6 +- src/mappings/services/assetCashflowService.ts | 11 +-- src/mappings/services/assetService.test.ts | 9 +-- src/mappings/services/assetService.ts | 9 +-- .../services/currencyBalanceService.ts | 7 +- src/mappings/services/currencyService.test.ts | 4 +- src/mappings/services/currencyService.ts | 5 +- src/mappings/services/epochService.ts | 18 ++--- .../services/investorTransactionService.ts | 2 +- .../services/outstandingOrderService.ts | 4 +- src/mappings/services/poolService.test.ts | 14 ++-- src/mappings/services/poolService.ts | 37 ++++------ .../services/trancheBalanceService.ts | 2 +- src/mappings/services/trancheService.test.ts | 15 ++-- src/mappings/services/trancheService.ts | 14 ++-- 22 files changed, 155 insertions(+), 199 deletions(-) diff --git a/src/@types/gobal.d.ts b/src/@types/gobal.d.ts index a8b57a8d..b9796363 100644 --- a/src/@types/gobal.d.ts +++ b/src/@types/gobal.d.ts @@ -2,11 +2,13 @@ import { ApiPromise } from '@polkadot/api' import type { Provider } from '@ethersproject/providers' import { ApiDecoration } from '@polkadot/api/types' import '@subql/types-core/dist/global' +import { ExtendedCall, ExtendedRpc } from '../helpers/types' export type ApiAt = ApiDecoration<'promise'> & { - rpc: ApiPromise['rpc'] + rpc: ApiPromise['rpc'] & ExtendedRpc + call: ApiPromise['call'] & ExtendedCall } declare global { - const api: ApiAt | Provider + const api: ApiAt & Provider const unsafeApi: ApiPromise | undefined function getNodeEvmChainId(): Promise } diff --git a/src/helpers/stateSnapshot.ts b/src/helpers/stateSnapshot.ts index 82cf1306..9c41e7e7 100644 --- a/src/helpers/stateSnapshot.ts +++ b/src/helpers/stateSnapshot.ts @@ -82,10 +82,45 @@ export function evmStateSnapshotter, ->( +export async function statesSnapshotter>( + relationshipField: StringForeignKeys, + relationshipId: string, + stateEntities: T[], + snapshotModel: EntityClass, + blockInfo: BlockInfo, + fkReferenceField?: StringForeignKeys, + resetPeriodStates = true +): Promise { + const entitySaves: Promise[] = [] + logger.info(`Performing ${snapshotModel.prototype._name}`) + if (stateEntities.length === 0) logger.info('Nothing to snapshot!') + for (const stateEntity of stateEntities) { + const blockNumber = blockInfo.number + const snapshot: SnapshottedEntity = { + ...stateEntity, + id: `${stateEntity.id}-${blockNumber}`, + timestamp: blockInfo.timestamp, + blockNumber: blockNumber, + [relationshipField]: relationshipId, + } + logger.info(`Creating ${snapshotModel.prototype._name} for: ${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 ResettableKeys[] + if (resetPeriodStates) { + for (const propName of propNamesToReset) { + logger.debug(`resetting ${stateEntity._name?.toLowerCase()}.${propName} to 0`) + stateEntity[propName] = BigInt(0) as T[ResettableKeys] + } + entitySaves.push(stateEntity.save()) + } + entitySaves.push(snapshotEntity.save()) + } + return Promise.all(entitySaves) +} + +export function substrateStateSnapshotter>( relationshipField: StringForeignKeys, relationshipId: string, stateModel: EntityClass, @@ -140,3 +175,8 @@ export interface EntityClass { getByFields(filter: FieldsExpression>[], options: GetOptions>): Promise create(record: EntityProps): T } + +export interface BlockInfo { + number: number + timestamp: Date +} diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 8292712f..a52da145 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -3,7 +3,6 @@ import { AugmentedCall, AugmentedRpc, PromiseRpcResult } from '@polkadot/api/typ import { Enum, Null, Struct, u128, u32, u64, U8aFixed, Option, Vec, Bytes, Result, bool } from '@polkadot/types' import { AccountId32, Perquintill, Balance } from '@polkadot/types/interfaces' import { ITuple, Observable } from '@polkadot/types/types' -import type { ApiAt } from '../@types/gobal' export interface PoolDetails extends Struct { reserve: { total: u128; available: u128; max: u128 } @@ -504,7 +503,7 @@ export type PoolFeesList = Vec export type OracleFedEvent = ITuple<[feeder: DevelopmentRuntimeOriginCaller, key: OracleKey, value: u128]> -export type ExtendedRpc = ApiAt['rpc'] & { +export type ExtendedRpc = { pools: { trancheTokenPrice: PromiseRpcResult< AugmentedRpc<(poolId: number | string, trancheId: number[]) => Observable> @@ -513,7 +512,7 @@ export type ExtendedRpc = ApiAt['rpc'] & { } } -export type ExtendedCall = ApiAt['call'] & { +export type ExtendedCall = { loansApi: { portfolio: AugmentedCall<'promise', (poolId: string) => Observable>>> expectedCashflows: AugmentedCall< diff --git a/src/mappings/handlers/blockHandlers.ts b/src/mappings/handlers/blockHandlers.ts index 146cbfc5..db9eecd7 100644 --- a/src/mappings/handlers/blockHandlers.ts +++ b/src/mappings/handlers/blockHandlers.ts @@ -1,31 +1,20 @@ import { SubstrateBlock } from '@subql/types' import { getPeriodStart, TimekeeperService } from '../../helpers/timekeeperService' import { errorHandler } from '../../helpers/errorHandler' -import { substrateStateSnapshotter } from '../../helpers/stateSnapshot' +import { statesSnapshotter } from '../../helpers/stateSnapshot' import { SNAPSHOT_INTERVAL_SECONDS } from '../../config' import { PoolService } from '../services/poolService' import { TrancheService } from '../services/trancheService' import { AssetService } from '../services/assetService' import { PoolFeeService } from '../services/poolFeeService' import { PoolFeeTransactionService } from '../services/poolFeeTransactionService' -import { - Asset, - AssetSnapshot, - Pool, - PoolFee, - PoolFeeSnapshot, - PoolSnapshot, - Tranche, - TrancheSnapshot, -} from '../../types/models' +import { AssetSnapshot, PoolFeeSnapshot, PoolSnapshot, TrancheSnapshot } from '../../types/models' import { AssetPositionService } from '../services/assetPositionService' import { EpochService } from '../services/epochService' import { SnapshotPeriodService } from '../services/snapshotPeriodService' import { TrancheBalanceService } from '../services/trancheBalanceService' import { InvestorPositionService } from '../services/investorPositionService' -import { ApiAt } from '../../@types/gobal' -const cfgApi = api as ApiAt const timekeeper = TimekeeperService.init() export const handleBlock = errorHandler(_handleBlock) @@ -37,7 +26,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { const newPeriod = (await timekeeper).processBlock(block.timestamp) if (newPeriod) { - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() logger.info( `# It's a new period on block ${blockNumber}: ${block.timestamp.toISOString()} (specVersion: ${specVersion})` ) @@ -54,6 +43,11 @@ async function _handleBlock(block: SubstrateBlock): Promise { const beginningOfQuarter = new Date(period.year, quarter * 3, 1) const beginningOfYear = new Date(period.year, 0, 1) + const poolsToSnapshot: PoolService[] = [] + const tranchesToSnapshot: TrancheService[] = [] + const assetsToSnapshot: AssetService[] = [] + const poolFeesToSnapshot: PoolFeeService[] = [] + // Update Pool States const pools = await PoolService.getCfgActivePools() for (const pool of pools) { @@ -83,6 +77,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { await tranche.computeYieldAnnualized('yield30DaysAnnualized', period.start, daysAgo30) await tranche.computeYieldAnnualized('yield90DaysAnnualized', period.start, daysAgo90) await tranche.save() + tranchesToSnapshot.push(tranche) // Compute TrancheBalances Unrealized Profit const trancheBalances = (await TrancheBalanceService.getByTrancheId(tranche.id, { @@ -105,7 +100,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { pool.resetUnrealizedProfit() for (const loanId in activeLoanData) { const asset = await AssetService.getById(pool.id, loanId) - if(!asset) continue + if (!asset) continue 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) @@ -115,6 +110,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { await AssetPositionService.computeUnrealizedProfitAtPrice(asset.id, asset.notional) ) await asset.save() + assetsToSnapshot.push(asset) if (typeof asset.interestAccruedByPeriod !== 'bigint') throw new Error('Asset interest accrued by period not set') @@ -158,6 +154,7 @@ async function _handleBlock(block: SubstrateBlock): Promise { } await poolFee.updateAccruals(pending, disbursement) await poolFee.save() + poolFeesToSnapshot.push(poolFee) if (typeof poolFee.sumAccruedAmountByPeriod !== 'bigint') throw new Error('Pool fee sum accrued amount by period not set') @@ -177,47 +174,16 @@ async function _handleBlock(block: SubstrateBlock): Promise { const sumPoolFeesPendingAmount = await PoolFeeService.computeSumPendingFees(pool.id) await pool.updateSumPoolFeesPendingAmount(sumPoolFeesPendingAmount) await pool.save() + poolsToSnapshot.push(pool) logger.info(`## Pool ${pool.id} states update completed!`) } 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, - Tranche, - TrancheSnapshot, - block, - [['isActive', '=', true]], - 'trancheId' - ) - await substrateStateSnapshotter( - 'periodId', - period.id, - Asset, - AssetSnapshot, - block, - [['isActive', '=', true]], - 'assetId' - ) - await substrateStateSnapshotter( - 'periodId', - period.id, - PoolFee, - PoolFeeSnapshot, - block, - [['isActive', '=', true]], - 'poolFeeId' - ) + const blockInfo = { number: block.block.header.number.toNumber(), timestamp: block.timestamp } + await statesSnapshotter('periodId', period.id, pools, PoolSnapshot, blockInfo, 'poolId') + await statesSnapshotter('periodId', period.id, tranchesToSnapshot, TrancheSnapshot, blockInfo, 'trancheId') + await statesSnapshotter('periodId', period.id, assetsToSnapshot, AssetSnapshot, blockInfo, 'assetId') + await statesSnapshotter('periodId', period.id, poolFeesToSnapshot, PoolFeeSnapshot, blockInfo, 'poolFeeId') logger.info('## Snapshotting completed!') //Update tracking of period and continue diff --git a/src/mappings/handlers/ethHandlers.ts b/src/mappings/handlers/ethHandlers.ts index f74ce79e..a67629af 100644 --- a/src/mappings/handlers/ethHandlers.ts +++ b/src/mappings/handlers/ethHandlers.ts @@ -1,4 +1,4 @@ -import { AssetStatus, AssetType, AssetValuationMethod, Pool, PoolSnapshot } from '../../types' +import { AssetStatus, AssetType, AssetValuationMethod, PoolSnapshot } from '../../types' import { EthereumBlock } from '@subql/types-ethereum' import { DAIName, DAISymbol, DAIMainnetAddress, multicallAddress, tinlakePools } from '../../config' import { errorHandler, missingPool } from '../../helpers/errorHandler' @@ -15,7 +15,7 @@ import { } from '../../types/contracts' import { TimekeeperService, getPeriodStart } from '../../helpers/timekeeperService' import { AssetService } from '../services/assetService' -import { evmStateSnapshotter } from '../../helpers/stateSnapshot' +import { BlockInfo, statesSnapshotter } from '../../helpers/stateSnapshot' import { Multicall3 } from '../../types/contracts/MulticallAbi' import type { Provider } from '@ethersproject/providers' import type { BigNumber } from '@ethersproject/bignumber' @@ -42,16 +42,23 @@ async function _handleEthBlock(block: EthereumBlock): Promise { await snapshotPeriod.save() // update pool states - const processedPools: PoolService[] = [] + const processedPools: Record< + PoolService['id'], + { + pool: PoolService + tinlakePool: typeof tinlakePools[0] + latestNavFeed?: ContractArray + latestReserve?: ContractArray + } + > = {} const poolUpdateCalls: PoolMulticall[] = [] for (const tinlakePool of tinlakePools) { if (blockNumber < tinlakePool.startBlock) continue const pool = await PoolService.getOrSeed(tinlakePool.id, false, false, blockchain.id) - processedPools.push(pool) - const latestNavFeed = getLatestContract(tinlakePool.navFeed, blockNumber) const latestReserve = getLatestContract(tinlakePool.reserve, blockNumber) + processedPools[pool.id] = { pool, latestNavFeed, latestReserve, tinlakePool } // initialize new pool if (!pool.isActive) { @@ -100,15 +107,7 @@ async function _handleEthBlock(block: EthereumBlock): Promise { }) for (const callResult of callResults) { - // const tinlakePool = tinlakePools.find((p) => p.id === callResult.id) - // if (!tinlakePool) throw missingPool - const pool = processedPools.find( p => callResult.id === p.id) - if (!pool) throw missingPool - - const tinlakePool = tinlakePools.find((p) => p.id === pool.id) - const latestNavFeed = getLatestContract(tinlakePool!.navFeed, blockNumber) - const latestReserve = getLatestContract(tinlakePool!.navFeed, blockNumber) - + const { pool, latestNavFeed, latestReserve, tinlakePool } = processedPools[callResult.id] // Update pool vurrentNav if (callResult.type === 'currentNAV' && latestNavFeed) { const currentNAV = @@ -121,7 +120,6 @@ async function _handleEthBlock(block: EthereumBlock): Promise { ? BigInt(0) : (pool.portfolioValuation ?? BigInt(0)) + (pool.totalReserve ?? BigInt(0)) await pool.updateNormalizedNAV() - await pool.save() logger.info(`Updating pool ${pool.id} with portfolioValuation: ${pool.portfolioValuation}`) } @@ -134,7 +132,6 @@ async function _handleEthBlock(block: EthereumBlock): Promise { pool.totalReserve = totalBalance pool.netAssetValue = (pool.portfolioValuation ?? BigInt(0)) + (pool.totalReserve ?? BigInt(0)) await pool.updateNormalizedNAV() - await pool.save() logger.info(`Updating pool ${pool.id} with totalReserve: ${pool.totalReserve}`) } @@ -149,19 +146,14 @@ async function _handleEthBlock(block: EthereumBlock): Promise { latestNavFeed.address ) } + + await pool.save() } // Take snapshots - await evmStateSnapshotter( - 'periodId', - snapshotPeriod.id, - Pool, - PoolSnapshot, - block, - [['isActive', '=', true]], - 'poolId' - ) - //await evmStateSnapshotter('Asset', 'AssetSnapshot', block, 'isActive', true, 'assetId') + const blockInfo: BlockInfo = { timestamp: date, number: block.number } + const poolsToSnapshot: PoolService[] = Object.values(processedPools).map(e => e.pool) + await statesSnapshotter('periodId', snapshotPeriod.id, poolsToSnapshot, PoolSnapshot, blockInfo, 'poolId') //Update tracking of period and continue await (await timekeeper).update(snapshotPeriod.start) @@ -188,7 +180,7 @@ async function updateLoans( logger.info(`Found ${newLoans.length} new loans for pool ${poolId}`) const pool = await PoolService.getById(poolId) - if(!pool) throw missingPool + if (!pool) throw missingPool const isAlt1AndAfterEndBlock = poolId === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK const nftIdCalls: PoolMulticall[] = [] diff --git a/src/mappings/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index b7248c55..98c736c9 100644 --- a/src/mappings/handlers/loansHandlers.ts +++ b/src/mappings/handlers/loansHandlers.ts @@ -22,9 +22,6 @@ import { WAD } from '../../config' import { AssetPositionService } from '../services/assetPositionService' import { AssetCashflowService } from '../services/assetCashflowService' import { assertPropInitialized } from '../../helpers/validation' -import { ApiAt } from '../../@types/gobal' - -const cfgApi = api as ApiAt export const handleLoanCreated = errorHandler(_handleLoanCreated) async function _handleLoanCreated(event: SubstrateEvent) { @@ -117,7 +114,7 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr 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 = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -198,7 +195,7 @@ 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 = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool @@ -338,7 +335,7 @@ async function _handleLoanClosed(event: SubstrateEvent) { export const handleLoanDebtTransferred = errorHandler(_handleLoanDebtTransferred) async function _handleLoanDebtTransferred(event: SubstrateEvent) { - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() const [poolId, fromLoanId, toLoanId, _repaidAmount, _borrowAmount] = event.event.data const timestamp = event.block.timestamp diff --git a/src/mappings/handlers/poolsHandlers.ts b/src/mappings/handlers/poolsHandlers.ts index b65e5eea..3ea49cef 100644 --- a/src/mappings/handlers/poolsHandlers.ts +++ b/src/mappings/handlers/poolsHandlers.ts @@ -11,8 +11,8 @@ import { TrancheBalanceService } from '../services/trancheBalanceService' import { BlockchainService, LOCAL_CHAIN_ID } from '../services/blockchainService' import { AssetService, ONCHAIN_CASH_ASSET_ID } from '../services/assetService' import { AssetTransactionData, AssetTransactionService } from '../services/assetTransactionService' -import { substrateStateSnapshotter } from '../../helpers/stateSnapshot' -import { Pool, PoolSnapshot } from '../../types' +import { statesSnapshotter } from '../../helpers/stateSnapshot' +import { PoolSnapshot } from '../../types' import { InvestorPositionService } from '../services/investorPositionService' import { PoolFeeService } from '../services/poolFeeService' import { assertPropInitialized } from '../../helpers/validation' @@ -178,7 +178,7 @@ async function _handleEpochExecuted(event: SubstrateEvent Promise.resolve('2030') + // eslint-disable-next-line @typescript-eslint/no-explicit-any -cfgApi.query['evmChainId'] = { chainId: jest.fn(() => ({ toString: () => '2030' })) } as any +api.query['evmChainId'] = { chainId: jest.fn(() => ({ toString: () => '2030' })) } as any test('Account is created in database', async () => { const id = 'ABCDE' diff --git a/src/mappings/services/assetCashflowService.ts b/src/mappings/services/assetCashflowService.ts index 457987ca..a45a879a 100644 --- a/src/mappings/services/assetCashflowService.ts +++ b/src/mappings/services/assetCashflowService.ts @@ -1,9 +1,5 @@ -import type { ApiAt } from '../../@types/gobal' -import { ExtendedCall } from '../../helpers/types' import { AssetCashflow } from '../../types/models/AssetCashflow' -const cfgApi = api as ApiAt - export class AssetCashflowService extends AssetCashflow { static init(assetId: string, timestamp: Date, principal: bigint, interest: bigint) { const id = `${assetId}-${timestamp.valueOf()}` @@ -12,13 +8,12 @@ export class AssetCashflowService extends AssetCashflow { } static async recordAssetCashflows(_assetId: string) { - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() if (specVersion < 1103) return const [poolId, assetId] = _assetId.split('-') logger.info(`Recording AssetCashflows for Asset ${_assetId}`) - const apiCall = cfgApi.call as ExtendedCall - logger.info(`Calling runtime API loanscfgApi.expectedCashflows(${poolId}, ${assetId})`) - const response = await apiCall.loansApi.expectedCashflows(poolId, assetId) + logger.info(`Calling runtime API loansapi.expectedCashflows(${poolId}, ${assetId})`) + const response = await api.call.loansApi.expectedCashflows(poolId, assetId) logger.info(JSON.stringify(response)) if(!response.isOk) return await this.clearAssetCashflows(_assetId) diff --git a/src/mappings/services/assetService.test.ts b/src/mappings/services/assetService.test.ts index 4d72ca77..06dc8fee 100644 --- a/src/mappings/services/assetService.test.ts +++ b/src/mappings/services/assetService.test.ts @@ -1,9 +1,6 @@ -import { ApiAt } from '../../@types/gobal' import { AssetType, AssetValuationMethod } from '../../types' import { AssetService } from './assetService' -const cfgApi = api as ApiAt - const poolId = '1111111111' const loanId = 'ABCD' const nftClassId = BigInt(1) @@ -11,12 +8,12 @@ const nftItemId = BigInt(2) const timestamp = new Date() const metadata = 'AAAAAA' -cfgApi.query['uniques'] = { +api.query['uniques']= { instanceMetadataOf: jest.fn(() => ({ isNone: false, unwrap: () => ({ data: { toUtf8: () => metadata } }), })), - // eslint-disable-next-line @typescript-eslint/no-explicit-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any } as any const loan = AssetService.init( @@ -44,7 +41,7 @@ describe('Given a new loan, when initialised', () => { test('when the metadata is fetched, then the correct values are set', async () => { await loan.updateItemMetadata() - expect(cfgApi.query.uniques.instanceMetadataOf).toHaveBeenCalledWith(nftClassId, nftItemId) + expect(api.query.uniques.instanceMetadataOf).toHaveBeenCalledWith(nftClassId, nftItemId) expect(loan.metadata).toBe(metadata) }) diff --git a/src/mappings/services/assetService.ts b/src/mappings/services/assetService.ts index 9e388c56..8ee99102 100644 --- a/src/mappings/services/assetService.ts +++ b/src/mappings/services/assetService.ts @@ -6,9 +6,6 @@ import { Asset, AssetType, AssetValuationMethod, AssetStatus, AssetSnapshot } fr import { ActiveLoanData } from './poolService' import { cid, readIpfs } from '../../helpers/ipfsFetch' import { assertPropInitialized } from '../../helpers/validation' -import { ApiAt } from '../../@types/gobal' - -const cfgApi = api as ApiAt export const ONCHAIN_CASH_ASSET_ID = '0' export class AssetService extends Asset { @@ -160,7 +157,7 @@ export class AssetService extends Asset { public async updateActiveAssetData(activeAssetData: ActiveLoanData[keyof ActiveLoanData]) { // Current price was always 0 until spec version 1025 - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() if (specVersion < 1025) delete activeAssetData.currentPrice // Set all active asset values @@ -187,7 +184,7 @@ export class AssetService extends Asset { `collectionId ${this.collateralNftClassId!.toString()}, ` + `itemId: ${this.collateralNftItemId!.toString()}` ) - const itemMetadata = await cfgApi.query.uniques.instanceMetadataOf>( + const itemMetadata = await api.query.uniques.instanceMetadataOf>( this.collateralNftClassId, this.collateralNftItemId ) @@ -253,7 +250,7 @@ export class AssetService extends Asset { public async updateExternalAssetPricingFromState() { logger.info(`Executing state call loans.activeLoans to update asset ${this.id} pricing information`) - const loansCall = await cfgApi.query.loans.activeLoans(this.poolId) + const loansCall = await api.query.loans.activeLoans(this.poolId) const assetTuple = loansCall.find((tuple) => tuple[0].toString(10) === this.id.split('-')[1]) if (!assetTuple) throw new Error(`Asset ${this.id} not found in pool active loans!`) const loanData = assetTuple[1] diff --git a/src/mappings/services/currencyBalanceService.ts b/src/mappings/services/currencyBalanceService.ts index 332a8625..fd5ad93f 100644 --- a/src/mappings/services/currencyBalanceService.ts +++ b/src/mappings/services/currencyBalanceService.ts @@ -1,10 +1,7 @@ -import { ApiAt } from '../../@types/gobal' import { AccountData } from '../../helpers/types' import { CurrencyBalance } from '../../types/models/CurrencyBalance' import { formatEnumPayload } from './currencyService' -const cfgApi = api as ApiAt - export class CurrencyBalanceService extends CurrencyBalance { static init(address: string, currency: string) { logger.info(`Initialising new CurrencyBalance: ${address}-${currency} to 0`) @@ -15,7 +12,7 @@ export class CurrencyBalanceService extends CurrencyBalance { static async getById(address: string, currency: string) { const id = `${address}-${currency}` const currencyBalance = await this.get(id) - return currencyBalance as CurrencyBalanceService + return currencyBalance as CurrencyBalanceService | undefined } static async getOrInit(address: string, currency: string) { @@ -30,7 +27,7 @@ export class CurrencyBalanceService extends CurrencyBalance { public async getBalance() { const [_chainId, currencyType, ...currencySpec] = this.currencyId.split('-') const enumPayload = formatEnumPayload(currencyType, ...currencySpec) - const balanceResponse = await cfgApi.query.ormlTokens.accounts(this.accountId, enumPayload) + const balanceResponse = await api.query.ormlTokens.accounts(this.accountId, enumPayload) this.amount = balanceResponse.free.toBigInt() logger.info(`Fetched initial balance of for CurrencyBalance ${this.id} of ${this.amount.toString(10)}`) } diff --git a/src/mappings/services/currencyService.test.ts b/src/mappings/services/currencyService.test.ts index be815990..21d0f066 100644 --- a/src/mappings/services/currencyService.test.ts +++ b/src/mappings/services/currencyService.test.ts @@ -1,11 +1,9 @@ -import { ApiAt } from '../../@types/gobal' import { CurrencyService } from './currencyService' -const cfgApi = api as ApiAt const entityName = 'Currency' // eslint-disable-next-line @typescript-eslint/no-explicit-any -cfgApi.query['ormlAssetRegistry'] = { metadata: jest.fn(() => ({ isSome: false })) } as any +api.query['ormlAssetRegistry'] = { metadata: jest.fn(() => ({ isSome: false })) } as any const stdDecimals = 18 diff --git a/src/mappings/services/currencyService.ts b/src/mappings/services/currencyService.ts index 7a4bbc69..d58bd645 100644 --- a/src/mappings/services/currencyService.ts +++ b/src/mappings/services/currencyService.ts @@ -3,9 +3,6 @@ import { AssetMetadata } from '@polkadot/types/interfaces' import { Currency } from '../../types/models/Currency' import { WAD_DIGITS } from '../../config' import type { TokensCurrencyId } from '../../helpers/types' -import { ApiAt } from '../../@types/gobal' - -const cfgApi = api as ApiAt export class CurrencyService extends Currency { static init(chainId: string, currencyId: string, decimals: number, symbol?: string, name?: string) { @@ -23,7 +20,7 @@ export class CurrencyService extends Currency { let currency: CurrencyService = (await this.get(id)) as CurrencyService if (!currency) { const enumPayload = formatEnumPayload(currencyType, ...currencyValue) - const assetMetadata = (await cfgApi.query.ormlAssetRegistry.metadata(enumPayload)) as Option + const assetMetadata = (await api.query.ormlAssetRegistry.metadata(enumPayload)) as Option const decimals = assetMetadata.isSome ? assetMetadata.unwrap().decimals.toNumber() : WAD_DIGITS const symbol = assetMetadata.isSome ? assetMetadata.unwrap().symbol.toUtf8() : undefined const name = assetMetadata.isSome ? assetMetadata.unwrap().name.toUtf8() : undefined diff --git a/src/mappings/services/epochService.ts b/src/mappings/services/epochService.ts index 7b499b8e..ae3edd8e 100644 --- a/src/mappings/services/epochService.ts +++ b/src/mappings/services/epochService.ts @@ -5,10 +5,6 @@ import { WAD } from '../../config' import { OrdersFulfillment } from '../../helpers/types' import { Epoch, EpochState } from '../../types' import { assertPropInitialized } from '../../helpers/validation' -import { ApiAt } from '../../@types/gobal' - -const cfgApi = api as ApiAt - export class EpochService extends Epoch { private states: EpochState[] @@ -69,23 +65,27 @@ export class EpochService extends Epoch { } public async executeEpoch(timestamp: Date) { + const specVersion = api.runtimeVersion.specVersion.toNumber() logger.info(`Updating Epoch OrderFulfillmentData for pool ${this.poolId} on epoch ${this.index}`) this.executedAt = timestamp for (const epochState of this.states) { logger.info(`Fetching data for tranche: ${epochState.trancheId}`) - const trancheCurrency = [this.poolId, epochState.trancheId] + const trancheCurrency = + specVersion < 1400 + ? { poolId: this.poolId, trancheId: epochState.trancheId } + : [this.poolId, epochState.trancheId] const [investOrderId, redeemOrderId] = await Promise.all([ - cfgApi.query.investments.investOrderId(trancheCurrency), - cfgApi.query.investments.redeemOrderId(trancheCurrency), + api.query.investments.investOrderId(trancheCurrency), + api.query.investments.redeemOrderId(trancheCurrency), ]) logger.info(`investOrderId: ${investOrderId.toNumber()}, redeemOrderId: ${redeemOrderId.toNumber()}`) const [investOrderFulfillment, redeemOrderFulfillment] = await Promise.all([ - cfgApi.query.investments.clearedInvestOrders>( + api.query.investments.clearedInvestOrders>( trancheCurrency, investOrderId.toNumber() - 1 ), - cfgApi.query.investments.clearedRedeemOrders>( + api.query.investments.clearedRedeemOrders>( trancheCurrency, redeemOrderId.toNumber() - 1 ), diff --git a/src/mappings/services/investorTransactionService.ts b/src/mappings/services/investorTransactionService.ts index 05636fd6..116b02c7 100644 --- a/src/mappings/services/investorTransactionService.ts +++ b/src/mappings/services/investorTransactionService.ts @@ -162,7 +162,7 @@ export class InvestorTransactionService extends InvestorTransaction { } static async getById(hash: string) { - const tx = (await this.get(hash)) as InvestorTransactionService + const tx = (await this.get(hash)) as InvestorTransactionService | undefined return tx } diff --git a/src/mappings/services/outstandingOrderService.ts b/src/mappings/services/outstandingOrderService.ts index 8d159da1..8f93fdea 100644 --- a/src/mappings/services/outstandingOrderService.ts +++ b/src/mappings/services/outstandingOrderService.ts @@ -29,12 +29,12 @@ export class OutstandingOrderService extends OutstandingOrder { static async getById(poolId: string, trancheId: string, address: string) { const oo = await this.get(`${poolId}-${trancheId}-${address}`) - return oo as OutstandingOrderService + return oo as OutstandingOrderService | undefined } static async getOrInit(data: InvestorTransactionData) { let oo = await this.getById(data.poolId, data.trancheId, data.address) - if (oo === undefined) oo = this.initZero(data) + if (!oo) oo = this.initZero(data) return oo } diff --git a/src/mappings/services/poolService.test.ts b/src/mappings/services/poolService.test.ts index 1beb35f2..a9d7b5bd 100644 --- a/src/mappings/services/poolService.test.ts +++ b/src/mappings/services/poolService.test.ts @@ -1,8 +1,6 @@ -import { ApiAt } from '../../@types/gobal' import { PoolService } from './poolService' -const cfgApi = api as ApiAt -cfgApi.query['poolSystem'] = { +api.query['poolSystem'] = { pool: jest.fn(() => ({ isSome: true, isNone: false, @@ -23,7 +21,7 @@ cfgApi.query['poolSystem'] = { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any -cfgApi.query['poolRegistry'] = { +api.query['poolRegistry'] = { poolMetadata: jest.fn(() => ({ isSome: true, isNone: false, @@ -32,7 +30,7 @@ cfgApi.query['poolRegistry'] = { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any -cfgApi.query['loans'] = { +api.query['loans'] = { portfolioValuation: jest.fn(() => ({ value: { toBigInt: () => BigInt(100000000000000), @@ -71,7 +69,7 @@ describe('Given a new pool, when initialised', () => { test('when the pool data is initialised, then the correct values are fetched and set', async () => { await pool.initData() - expect(cfgApi.query.poolSystem.pool).toBeCalledWith(poolId) + expect(api.query.poolSystem.pool).toBeCalledWith(poolId) expect(pool).toMatchObject({ currencyId: 'AUSD', metadata: 'AAA', @@ -89,13 +87,13 @@ describe('Given a new pool, when initialised', () => { describe('Given an existing pool,', () => { test.skip('when the nav is updated, then the value is fetched and set correctly', async () => { await pool.updateNAV() - expect(cfgApi.query.loans.portfolioValuation).toHaveBeenCalled() + expect(api.query.loans.portfolioValuation).toHaveBeenCalled() expect(pool.portfolioValuation).toBe(BigInt(100000000000000)) }) test('when the pool state is updated, then the values are fetched and set correctly', async () => { await pool.updateState() - expect(cfgApi.query.poolSystem.pool).toHaveBeenCalledWith(poolId) + expect(api.query.poolSystem.pool).toHaveBeenCalledWith(poolId) expect(pool).toMatchObject({ totalReserve: BigInt(91000000000000), availableReserve: BigInt(92000000000000), diff --git a/src/mappings/services/poolService.ts b/src/mappings/services/poolService.ts index 72707239..0c6128db 100644 --- a/src/mappings/services/poolService.ts +++ b/src/mappings/services/poolService.ts @@ -1,7 +1,6 @@ import { Option, u128, Vec } from '@polkadot/types' import { paginatedGetter } from '../../helpers/paginatedGetter' import type { - ExtendedCall, NavDetails, PoolDetails, PoolFeesList, @@ -16,9 +15,6 @@ import { EpochService } from './epochService' import { WAD_DIGITS } from '../../config' import { CurrencyService } from './currencyService' import { assertPropInitialized } from '../../helpers/validation' -import { ApiAt } from '../../@types/gobal' - -const cfgApi = api as ApiAt export class PoolService extends Pool { static seed(poolId: string, blockchain = '0') { @@ -137,8 +133,8 @@ export class PoolService extends Pool { public async initData() { logger.info(`Initialising data for pool: ${this.id}`) const [poolReq, metadataReq] = await Promise.all([ - cfgApi.query.poolSystem.pool>(this.id), - cfgApi.query.poolRegistry.poolMetadata>(this.id), + api.query.poolSystem.pool>(this.id), + api.query.poolRegistry.poolMetadata>(this.id), ]) if (poolReq.isNone) throw new Error('No pool data available to create the pool') @@ -207,7 +203,7 @@ export class PoolService extends Pool { } public async updateState() { - const poolResponse = await cfgApi.query.poolSystem.pool>(this.id) + const poolResponse = await api.query.poolSystem.pool>(this.id) logger.info(`Updating state for pool: ${this.id}`) if (poolResponse.isSome) { const poolData = poolResponse.unwrap() @@ -219,8 +215,8 @@ export class PoolService extends Pool { } public async updateNAV() { - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() - const specName = cfgApi.runtimeVersion.specName.toString() + const specVersion = api.runtimeVersion.specVersion.toNumber() + const specName = api.runtimeVersion.specName.toString() switch (specName) { case 'centrifuge-devel': await (specVersion < 1038 ? this.updateNAVQuery() : this.updateNAVCall()) @@ -239,7 +235,7 @@ export class PoolService extends Pool { assertPropInitialized(this, 'portfolioValuation', 'bigint') assertPropInitialized(this, 'totalReserve', 'bigint') - const navResponse = await cfgApi.query.loans.portfolioValuation(this.id) + const navResponse = await api.query.loans.portfolioValuation(this.id) const newPortfolioValuation = navResponse.value.toBigInt() - this.offchainCashValue! this.deltaPortfolioValuationByPeriod = newPortfolioValuation - this.portfolioValuation! @@ -261,8 +257,7 @@ export class PoolService extends Pool { assertPropInitialized(this, 'offchainCashValue', 'bigint') assertPropInitialized(this, 'portfolioValuation', 'bigint') - const apiCall = api.call as ExtendedCall - const navResponse = await apiCall.poolsApi.nav(this.id) + const navResponse = await api.call.poolsApi.nav(this.id) if (navResponse.isEmpty) { logger.warn('Empty pv response') return @@ -399,7 +394,7 @@ export class PoolService extends Pool { public async fetchTranchesFrom1400(): Promise { logger.info(`Fetching tranches for pool: ${this.id} with specVersion >= 1400`) - const poolResponse = await cfgApi.query.poolSystem.pool>(this.id) + const poolResponse = await api.query.poolSystem.pool>(this.id) if (poolResponse.isNone) throw new Error('Unable to fetch pool data!') const poolData = poolResponse.unwrap() const { ids, tranches } = poolData.tranches @@ -431,7 +426,7 @@ export class PoolService extends Pool { public async fetchTranchesBefore1400(): Promise { logger.info(`Fetching tranches for pool: ${this.id} with specVersion < 1400`) - const poolResponse = await cfgApi.query.poolSystem.pool>(this.id) + const poolResponse = await api.query.poolSystem.pool>(this.id) if (poolResponse.isNone) throw new Error('Unable to fetch pool data!') const poolData = poolResponse.unwrap() const { ids, tranches } = poolData.tranches @@ -462,7 +457,7 @@ export class PoolService extends Pool { } public async getTranches() { - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() let tranches: TrancheData[] if (specVersion >= 1400) { tranches = await this.fetchTranchesFrom1400() @@ -473,9 +468,8 @@ export class PoolService extends Pool { } public async getPortfolio(): Promise { - const apiCall = api.call as ExtendedCall logger.info(`Querying runtime loansApi.portfolio for pool: ${this.id}`) - const portfolioData = await apiCall.loansApi.portfolio(this.id) + const portfolioData = await api.call.loansApi.portfolio(this.id) logger.info(`${portfolioData.length} assets found.`) return portfolioData.reduce((obj, current) => { const [assetId, asset] = current @@ -523,7 +517,7 @@ export class PoolService extends Pool { const poolId = this.id let tokenPrices: Vec try { - const apiRes = await (api.call as ExtendedCall).poolsApi.trancheTokenPrices(poolId) + const apiRes = await api.call.poolsApi.trancheTokenPrices(poolId) tokenPrices = apiRes.unwrap() } catch (err) { logger.error(`Unable to fetch tranche token prices for pool: ${this.id}: ${err}`) @@ -533,9 +527,8 @@ export class PoolService extends Pool { } public async getAccruedFees() { - const apiCall = cfgApi.call as ExtendedCall - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() - const specName = cfgApi.runtimeVersion.specName.toString() + const specVersion = api.runtimeVersion.specVersion.toNumber() + const specName = api.runtimeVersion.specName.toString() switch (specName) { case 'centrifuge-devel': if (specVersion < 1040) return [] @@ -545,7 +538,7 @@ export class PoolService extends Pool { break } logger.info(`Querying runtime poolFeesApi.listFees for pool ${this.id}`) - const poolFeesListRequest = await apiCall.poolFeesApi.listFees(this.id) + const poolFeesListRequest = await api.call.poolFeesApi.listFees(this.id) let poolFeesList: PoolFeesList try { poolFeesList = poolFeesListRequest.unwrap() diff --git a/src/mappings/services/trancheBalanceService.ts b/src/mappings/services/trancheBalanceService.ts index d3c6e967..1056a6de 100644 --- a/src/mappings/services/trancheBalanceService.ts +++ b/src/mappings/services/trancheBalanceService.ts @@ -21,7 +21,7 @@ export class TrancheBalanceService extends TrancheBalance { static async getById(address: string, poolId: string, trancheId: string) { const trancheBalance = await this.get(`${address}-${poolId}-${trancheId}`) - return trancheBalance as TrancheBalanceService + return trancheBalance as TrancheBalanceService | undefined } static getOrInit = async (address: string, poolId: string, trancheId: string) => { diff --git a/src/mappings/services/trancheService.test.ts b/src/mappings/services/trancheService.test.ts index 86818f7a..cf3f3c7e 100644 --- a/src/mappings/services/trancheService.test.ts +++ b/src/mappings/services/trancheService.test.ts @@ -1,18 +1,15 @@ -import { ApiAt } from '../../@types/gobal' import { errorLogger } from '../../helpers/errorHandler' -import { ExtendedCall } from '../../helpers/types' import { TrancheService } from './trancheService' -const cfgApi = api as ApiAt -cfgApi.query['ormlTokens'] = { +api.query['ormlTokens'] = { totalIssuance: jest.fn(() => ({ toBigInt: () => BigInt('9999000000000000000000') })), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any // eslint-disable-next-line @typescript-eslint/no-explicit-any -cfgApi['runtimeVersion'] = { specVersion: { toNumber: () => 1029 } } as any +api['runtimeVersion'] = { specVersion: { toNumber: () => 1029 } } as any -cfgApi.call['poolsApi'] = { +api.call['poolsApi'] = { trancheTokenPrices: jest.fn(() => ({ isSome: true, isNone: false, @@ -65,7 +62,7 @@ describe('Given a new tranche, when initialised', () => { test('when the supply data is fetched, then the correct values are fetched and set', async () => { await tranches[0].updateSupply() - expect(cfgApi.query.ormlTokens.totalIssuance).toHaveBeenCalledWith({ Tranche: [poolId, trancheIds[0]] }) + expect(api.query.ormlTokens.totalIssuance).toHaveBeenCalledWith({ Tranche: [poolId, trancheIds[0]] }) expect(tranches[0]).toMatchObject({ tokenSupply: BigInt('9999000000000000000000') }) }) @@ -78,13 +75,13 @@ describe('Given a new tranche, when initialised', () => { describe('Given an existing tranche,', () => { test('when the runtime price is updated, then the value is fetched and set correctly', async () => { await tranches[0].updatePriceFromRuntime(4058351).catch(errorLogger) - expect((api.call as ExtendedCall).poolsApi.trancheTokenPrices).toHaveBeenCalled() + expect(api.call.poolsApi.trancheTokenPrices).toHaveBeenCalled() expect(tranches[0].tokenPrice).toBe(BigInt('2000000000000000000')) }) test('when a 0 runtime price is delivered, then the value is skipped and logged', async () => { await tranches[1].updatePriceFromRuntime(4058352).catch(errorLogger) - expect((api.call as ExtendedCall).poolsApi.trancheTokenPrices).toHaveBeenCalled() + expect(api.call.poolsApi.trancheTokenPrices).toHaveBeenCalled() expect(logger.error).toHaveBeenCalled() expect(tranches[1].tokenPrice).toBe(BigInt('1000000000000000000')) }) diff --git a/src/mappings/services/trancheService.ts b/src/mappings/services/trancheService.ts index 1fcdcf8a..8ae371eb 100644 --- a/src/mappings/services/trancheService.ts +++ b/src/mappings/services/trancheService.ts @@ -2,12 +2,8 @@ import { u128 } from '@polkadot/types' import { bnToBn, nToBigInt } from '@polkadot/util' import { paginatedGetter } from '../../helpers/paginatedGetter' import { WAD } from '../../config' -import { ExtendedCall } from '../../helpers/types' import { Tranche, TrancheSnapshot } from '../../types' import { TrancheData } from './poolService' -import { ApiAt } from '../../@types/gobal' - -const cfgApi = api as ApiAt const MAINNET_CHAINID = '0xb3db41421702df9a7fcac62b53ffeac85f7853cc4e689e0b93aeb3db18c09d82' @@ -81,13 +77,13 @@ export class TrancheService extends Tranche { public async updateSupply() { logger.info(`Updating supply for tranche ${this.id}`) const requestPayload = { Tranche: [this.poolId, this.trancheId] } - const supplyResponse = await cfgApi.query.ormlTokens.totalIssuance(requestPayload) + const supplyResponse = await api.query.ormlTokens.totalIssuance(requestPayload) this.tokenSupply = supplyResponse.toBigInt() return this } public async updatePrice(price: bigint, block?: number) { - const specVersion = cfgApi.runtimeVersion.specVersion.toNumber() + const specVersion = api.runtimeVersion.specVersion.toNumber() if (MAINNET_CHAINID === chainId && !!block) { if (block < 4058350) return this.updatePriceFixDecimalError(price, block) if (specVersion >= 1025 && specVersion < 1029) return await this.updatePriceFixForFees(price) @@ -109,8 +105,7 @@ export class TrancheService extends Tranche { private async updatePriceFixForFees(price: bigint) { // fix token price not accounting for fees - const apiCall = api.call as ExtendedCall - const navResponse = await apiCall.poolsApi.nav(this.poolId) + const navResponse = await api.call.poolsApi.nav(this.poolId) if (navResponse.isEmpty) { logger.warn(`No NAV response! Saving inaccurate price: ${price} `) this.tokenPrice = price @@ -131,8 +126,7 @@ export class TrancheService extends Tranche { logger.info(`Querying token price for tranche ${this.id} from runtime`) const { poolId } = this - const apiCall = api.call as ExtendedCall - const tokenPricesReq = await apiCall.poolsApi.trancheTokenPrices(poolId) + const tokenPricesReq = await api.call.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() From 40e56d76f235035610442387d17ab31bdfc8cd71 Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Wed, 13 Nov 2024 10:24:32 +0100 Subject: [PATCH 5/6] fix: currency initialization --- src/mappings/services/accountService.ts | 2 +- src/mappings/services/currencyService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mappings/services/accountService.ts b/src/mappings/services/accountService.ts index fd7fdb6f..abcd0e6e 100644 --- a/src/mappings/services/accountService.ts +++ b/src/mappings/services/accountService.ts @@ -17,7 +17,7 @@ export class AccountService extends Account { } static async getOrInit(address: string, blockchainService = BlockchainService): Promise { - let account = (await this.get(address)) as AccountService + let account = (await this.get(address)) as AccountService | undefined if (!account) { account = await this.init(address) await blockchainService.getOrInit(account.chainId) diff --git a/src/mappings/services/currencyService.ts b/src/mappings/services/currencyService.ts index d58bd645..eabca03a 100644 --- a/src/mappings/services/currencyService.ts +++ b/src/mappings/services/currencyService.ts @@ -17,7 +17,7 @@ export class CurrencyService extends Currency { static async getOrInit(chainId: string, currencyType: string, ...currencyValue: string[]) { const currencyId = currencyValue.length > 0 ? `${currencyType}-${currencyValue.join('-')}` : currencyType const id = `${chainId}-${currencyId}` - let currency: CurrencyService = (await this.get(id)) as CurrencyService + let currency = (await this.get(id)) as CurrencyService | undefined if (!currency) { const enumPayload = formatEnumPayload(currencyType, ...currencyValue) const assetMetadata = (await api.query.ormlAssetRegistry.metadata(enumPayload)) as Option From cc28f31dd7711c520e3e4b90df7b497bc577052c Mon Sep 17 00:00:00 2001 From: Filippo Fontana Date: Wed, 13 Nov 2024 10:29:20 +0100 Subject: [PATCH 6/6] chore: upgrade indexer version --- .github/workflows/subql_multi_deploy_workflow.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/subql_multi_deploy_workflow.yaml b/.github/workflows/subql_multi_deploy_workflow.yaml index 55adde9e..626c5986 100644 --- a/.github/workflows/subql_multi_deploy_workflow.yaml +++ b/.github/workflows/subql_multi_deploy_workflow.yaml @@ -28,8 +28,8 @@ jobs: subql_deploy_workflow: runs-on: ubuntu-latest env: - SUBQL_CFG_INDEXER_VERSION: "v5.2.5" - SUBQL_ETH_INDEXER_VERSION: "v5.1.3" + SUBQL_CFG_INDEXER_VERSION: "v5.2.9" + SUBQL_ETH_INDEXER_VERSION: "v5.1.7" CHAIN_ID: ${{ inputs.chainId }} SUBQL_ACCESS_TOKEN: ${{ secrets.accessToken }} SUBQL_PROJ_ORG: ${{ inputs.projOrg }}