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 }} 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..b9796363 100644 --- a/src/@types/gobal.d.ts +++ b/src/@types/gobal.d.ts @@ -1,4 +1,15 @@ -export {} +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'] & ExtendedRpc + call: ApiPromise['call'] & ExtendedCall +} declare global { - function getNodeEvmChainId(): Promise + const api: ApiAt & Provider + const unsafeApi: ApiPromise | undefined + function getNodeEvmChainId(): Promise } +export {} 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/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..9c41e7e7 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,125 @@ 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 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, 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 +} + +export interface BlockInfo { + number: number + timestamp: Date +} diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 07eae3cb..a52da145 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 { @@ -497,7 +503,7 @@ export type PoolFeesList = Vec export type OracleFedEvent = ITuple<[feeder: DevelopmentRuntimeOriginCaller, key: OracleKey, value: u128]> -export type ExtendedRpc = typeof api.rpc & { +export type ExtendedRpc = { pools: { trancheTokenPrice: PromiseRpcResult< AugmentedRpc<(poolId: number | string, trancheId: number[]) => Observable> @@ -506,7 +512,7 @@ export type ExtendedRpc = typeof api.rpc & { } } -export type ExtendedCall = typeof api.call & { +export type ExtendedCall = { loansApi: { portfolio: AugmentedCall<'promise', (poolId: string) => Observable>>> expectedCashflows: AugmentedCall< 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..7acbe0fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,14 +5,14 @@ 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 +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..db9eecd7 100644 --- a/src/mappings/handlers/blockHandlers.ts +++ b/src/mappings/handlers/blockHandlers.ts @@ -1,23 +1,14 @@ 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' @@ -28,6 +19,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) @@ -50,11 +43,18 @@ 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) { 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 +63,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) @@ -77,12 +77,14 @@ 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, { 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 +100,9 @@ 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) await asset.updateActiveAssetData(activeLoanData[loanId]) await asset.updateUnrealizedProfit( @@ -105,15 +110,33 @@ 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') 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)) @@ -131,7 +154,10 @@ 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') await pool.increaseAccruedFees(poolFee.sumAccruedAmountByPeriod) const poolFeeTransaction = PoolFeeTransactionService.accrue({ @@ -148,33 +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 8cc868fb..a67629af 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 { AssetStatus, AssetType, AssetValuationMethod, 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' @@ -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' @@ -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) @@ -40,130 +33,130 @@ 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[] = [] - for (const tinlakePool of tinlakePools) { - if (block.number >= tinlakePool.startBlock) { - const pool = await PoolService.getOrSeed(tinlakePool.id, false, false, blockchain.id) + 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: 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) + 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) { + 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() + } - // initialize new pool - if (!pool.isActive) { - await pool.initTinlake(tinlakePool.shortName, currency.id, date, blockNumber) - await pool.save() + //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: '', + }) + } + } - 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() + //Execute available calls + const callResults: PoolMulticall[] = await processCalls(poolUpdateCalls).catch((err) => { + logger.error(`poolUpdateCalls failed: ${err}`) + return [] + }) - const junior = await TrancheService.getOrSeed(pool.id, 'junior', blockchain.id) - await junior.initTinlake(pool.id, `${pool.name} (Junior)`, 0) - await junior.save() - } + for (const callResult of callResults) { + const { pool, latestNavFeed, latestReserve, tinlakePool } = processedPools[callResult.id] + // 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() + logger.info(`Updating pool ${pool.id} with portfolioValuation: ${pool.portfolioValuation}`) + } - 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) { - poolUpdateCalls.push({ - id: tinlakePool.id, - type: 'totalBalance', - call: { - target: latestReserve.address, - callData: ReserveAbi__factory.createInterface().encodeFunctionData('totalBalance'), - }, - result: '', - }) - } - } + // 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() + logger.info(`Updating pool ${pool.id} with totalReserve: ${pool.totalReserve}`) } - if (poolUpdateCalls.length > 0) { - 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) - - // 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 && 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 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) + await pool.save() } + + // Take snapshots + 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) } type NewLoanData = { @@ -187,6 +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 const isAlt1AndAfterEndBlock = poolId === ALT_1_POOL_ID && blockNumber > ALT_1_END_BLOCK const nftIdCalls: PoolMulticall[] = [] @@ -203,7 +197,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 = { @@ -235,7 +232,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) @@ -277,7 +277,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) => { @@ -311,32 +311,33 @@ 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 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 + 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 +347,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 +363,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) + 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() 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() @@ -409,7 +407,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> @@ -430,12 +428,8 @@ 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.find((entry) => entry.startBlock <= blockNumber) } function chunkArray(array: T[], chunkSize: number): T[][] { @@ -447,20 +441,42 @@ 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 - let results: any[] = [] + for (const [i, chunk] of callChunks.entries()) { + const multicall = MulticallAbi__factory.connect(multicallAddress, api as Provider) + 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}`) } } - 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..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,16 +15,17 @@ 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) 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 +45,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 +57,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,8 +75,10 @@ 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) + if (!tranche) throw new Error('Tranche not found!') const orderData: Omit = { poolId: evmToken.poolId, @@ -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..7e1a37bf 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( @@ -25,10 +28,12 @@ async function _handleInvestOrderUpdated(event: SubstrateEvent BigInt(0)) { @@ -62,7 +67,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( @@ -89,9 +98,11 @@ async function _handleRedeemOrderUpdated(event: SubstrateEvent BigInt(0)) { @@ -125,8 +136,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 ` + @@ -157,6 +173,7 @@ 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()} ` + @@ -209,6 +229,7 @@ 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,13 +123,17 @@ 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 const asset = await AssetService.getById(poolId.toString(), loanId.toString()) + if (!asset) throw new Error('Unable to retrieve asset!') + await asset.activate() const assetTransactionBaseData = { @@ -131,11 +142,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 +168,8 @@ async function _handleLoanBorrowed(event: SubstrateEvent): Pr asset.id, assetTransactionBaseData.hash, assetTransactionBaseData.timestamp, - assetTransactionBaseData.quantity, - assetTransactionBaseData.settlementPrice + assetTransactionBaseData.quantity!, + assetTransactionBaseData.settlementPrice! ) } @@ -182,6 +193,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 +205,28 @@ 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()) - + if (!asset) throw new Error('Unable to retrieve asset!') 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 +256,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 @@ -265,13 +283,14 @@ 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() 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 +300,23 @@ 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()) + if (!asset) throw new Error('Unable to retrieve asset!') 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 +325,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 +337,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 +357,16 @@ 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 +504,16 @@ 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 +607,14 @@ 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()) + if (!asset) throw new Error('Unable to retrieve asset!') - 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 +622,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 +646,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 +662,14 @@ 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()) + if (!asset) throw new Error('Unable to retrieve asset!') - 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 +677,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..3ea49cef 100644 --- a/src/mappings/handlers/poolsHandlers.ts +++ b/src/mappings/handlers/poolsHandlers.ts @@ -11,14 +11,17 @@ 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' 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() @@ -52,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() } @@ -81,11 +86,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 +148,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,46 +174,51 @@ 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()}` + `at block ${event.block.block.header.number.toString()} wit hash ${event.block.hash.toHex()}` ) const pool = await PoolService.getById(poolId.toString()) if (!pool) throw missingPool const epoch = await EpochService.getById(poolId.toString(), epochId.toNumber()) + if (!epoch) throw new Error(`Epoch ${epochId.toString(10)} not found for pool ${poolId.toString(10)}`) - await epoch.executeEpoch(event.block.timestamp) + await epoch.executeEpoch(timestamp) await epoch.saveWithStates() await pool.executeEpoch(epochId.toNumber()) - await pool.increaseInvestments(epoch.sumInvestedAmount) - await pool.increaseRedemptions(epoch.sumRedeemedAmount) + await pool.increaseInvestments(epoch.sumInvestedAmount!) + await pool.increaseRedemptions(epoch.sumRedeemedAmount!) await pool.save() // Compute and save aggregated order fulfillment const tranches = await TrancheService.getByPoolId(poolId.toString()) const nextEpoch = await EpochService.getById(poolId.toString(), epochId.toNumber() + 1) + if (!nextEpoch) throw new Error(`Epoch ${epochId.toNumber() + 1} not found for pool ${poolId.toString(10)}`) for (const tranche of tranches) { const epochState = epoch.getStates().find((epochState) => 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 +234,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 +296,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 +322,7 @@ async function _handleEpochExecuted(event: SubstrateEvent BigInt(0)) { + if (epoch.sumPoolFeesPaidAmount! > BigInt(0)) { const withdrawalFees = await AssetTransactionService.withdrawalForFees({ ...txData, amount: epoch.sumPoolFeesPaidAmount, @@ -319,14 +334,12 @@ async function _handleEpochExecuted(event: SubstrateEvent Promise.resolve('2030') + // eslint-disable-next-line @typescript-eslint/no-explicit-any api.query['evmChainId'] = { chainId: jest.fn(() => ({ toString: () => '2030' })) } as any 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/assetCashflowService.ts b/src/mappings/services/assetCashflowService.ts index be284377..a45a879a 100644 --- a/src/mappings/services/assetCashflowService.ts +++ b/src/mappings/services/assetCashflowService.ts @@ -1,4 +1,3 @@ -import { ExtendedCall } from '../../helpers/types' import { AssetCashflow } from '../../types/models/AssetCashflow' export class AssetCashflowService extends AssetCashflow { @@ -13,9 +12,8 @@ export class AssetCashflowService extends AssetCashflow { if (specVersion < 1103) return const [poolId, assetId] = _assetId.split('-') logger.info(`Recording AssetCashflows for Asset ${_assetId}`) - const apiCall = api.call as ExtendedCall - logger.info(`Calling runtime API loansApi.expectedCashflows(${poolId}, ${assetId})`) - const response = await apiCall.loansApi.expectedCashflows(poolId, assetId) + 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 0035bec9..06dc8fee 100644 --- a/src/mappings/services/assetService.test.ts +++ b/src/mappings/services/assetService.test.ts @@ -8,12 +8,12 @@ const nftItemId = BigInt(2) const timestamp = new Date() const metadata = 'AAAAAA' -api.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( @@ -35,7 +35,7 @@ describe('Given a new loan, when initialised', () => { 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..8ee99102 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 { @@ -80,36 +81,43 @@ 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 } static async getByNftId(collectionId: string, itemId: string) { const asset = ( - await AssetService.getByFields([ - ['collateralNftClassId', '=', collectionId], - ['collateralNftItemId', '=', itemId], - ], { limit: 100 }) - ).pop() as AssetService + await AssetService.getByFields( + [ + ['collateralNftClassId', '=', collectionId], + ['collateralNftItemId', '=', itemId], + ], + { limit: 100 } + ) + ).pop() as AssetService | undefined 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/currencyBalanceService.ts b/src/mappings/services/currencyBalanceService.ts index 89439814..fd5ad93f 100644 --- a/src/mappings/services/currencyBalanceService.ts +++ b/src/mappings/services/currencyBalanceService.ts @@ -12,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) { diff --git a/src/mappings/services/currencyService.ts b/src/mappings/services/currencyService.ts index 7b6107f4..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 @@ -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..ae3edd8e 100644 --- a/src/mappings/services/epochService.ts +++ b/src/mappings/services/epochService.ts @@ -4,7 +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[] @@ -65,12 +65,16 @@ 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([ api.query.investments.investOrderId(trancheCurrency), api.query.investments.redeemOrderId(trancheCurrency), @@ -104,11 +108,13 @@ export class EpochService extends Epoch { ) epochState.sumFulfilledRedeemOrdersCurrency = this.computeCurrencyAmount( epochState.sumFulfilledRedeemOrders, - epochState.tokenPrice + 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 +123,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 +132,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 +147,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..116b02c7 100644 --- a/src/mappings/services/investorTransactionService.ts +++ b/src/mappings/services/investorTransactionService.ts @@ -162,16 +162,16 @@ 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 } 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..8f93fdea 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 @@ -27,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/poolFeeService.ts b/src/mappings/services/poolFeeService.ts index ea71b9ac..0fbfe8f0 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' @@ -39,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 { @@ -49,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) { @@ -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..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, @@ -15,6 +14,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 +140,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,26 +151,27 @@ 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 { + public async getIpfsPoolFeeMetadata(): Promise['poolFees']> { if (!this.metadata) return logger.warn('No IPFS metadata') - const metadata = await readIpfs(this.metadata.match(cid)[0]) - if (!metadata.pool.poolFees) { - return null - } - return metadata.pool.poolFees + 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 ?? [] } public async getIpfsPoolFeeName(poolFeeId: string): Promise { @@ -178,13 +179,13 @@ 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) { - return this.get(poolId) as Promise + return this.get(poolId) as Promise } static async getAll() { @@ -230,15 +231,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,16 +253,19 @@ export class PoolService extends Pool { private async updateNAVCall() { logger.info(`Updating portfolio valuation for pool: ${this.id} (runtime)`) - const apiCall = api.call as ExtendedCall - const navResponse = await apiCall.poolsApi.nav(this.id) + + assertPropInitialized(this, 'offchainCashValue', 'bigint') + assertPropInitialized(this, 'portfolioValuation', 'bigint') + + const navResponse = await api.call.poolsApi.nav(this.id) if (navResponse.isEmpty) { logger.warn('Empty pv response') 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 +276,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 +287,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 +306,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 +317,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 +376,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 { @@ -430,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 @@ -450,10 +487,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(), @@ -480,17 +517,16 @@ export class PoolService extends Pool { const poolId = this.id let tokenPrices: Vec try { - const apiRes = await (api.call as ExtendedCall).poolsApi.trancheTokenPrices(poolId) - tokenPrices = apiRes.isSome ? apiRes.unwrap() : undefined + 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}`) - tokenPrices = undefined + return undefined } return tokenPrices } public async getAccruedFees() { - const apiCall = api.call as ExtendedCall const specVersion = api.runtimeVersion.specVersion.toNumber() const specName = api.runtimeVersion.specName.toString() switch (specName) { @@ -502,8 +538,14 @@ 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 poolFeesList = poolFeesListRequest.unwrapOr([]) + const poolFeesListRequest = await api.call.poolFeesApi.listFees(this.id) + 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 +557,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 +602,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 +613,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 +624,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 +641,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/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 67687a53..cf3f3c7e 100644 --- a/src/mappings/services/trancheService.test.ts +++ b/src/mappings/services/trancheService.test.ts @@ -1,5 +1,4 @@ import { errorLogger } from '../../helpers/errorHandler' -import { ExtendedCall } from '../../helpers/types' import { TrancheService } from './trancheService' api.query['ormlTokens'] = { @@ -8,7 +7,7 @@ api.query['ormlTokens'] = { } as any // eslint-disable-next-line @typescript-eslint/no-explicit-any -api['runtimeVersion'] = { specVersion: { toNumber: ()=> 1029 } } as any +api['runtimeVersion'] = { specVersion: { toNumber: () => 1029 } } as any api.call['poolsApi'] = { trancheTokenPrices: jest.fn(() => ({ @@ -56,8 +55,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)) } }) @@ -76,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 342bc4a8..8ae371eb 100644 --- a/src/mappings/services/trancheService.ts +++ b/src/mappings/services/trancheService.ts @@ -2,7 +2,6 @@ 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' @@ -58,21 +57,21 @@ 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]]) - 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, [ + const tranches = (await paginatedGetter(this, [ ['poolId', '=', poolId], ['isActive', '=', true], - ]) - return tranches as TrancheService[] + ])) as TrancheService[] + return tranches } public async updateSupply() { @@ -106,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 @@ -128,9 +126,9 @@ 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() logger.info(`Token price: ${tokenPrice.toString()}`) if (tokenPrice <= BigInt(0)) throw new Error(`Zero or negative price returned for tranche: ${this.id}`) @@ -150,7 +148,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 +157,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 +215,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 +224,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 +236,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 +245,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 +284,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..1c25e74a 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" 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"