diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ab07f3fd..e9703437 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -63,5 +63,5 @@ jobs: --projectName="$SUBQL_PROJ_NAME" \ --ipfsCID="$IPFSCID" \ --type=primary \ - --indexerVersion="v1.6.1" \ + --indexerVersion="v1.9.1" \ --queryVersion="v1.5.0" diff --git a/project.yaml b/project.yaml index b969cb97..c0d5553e 100644 --- a/project.yaml +++ b/project.yaml @@ -22,7 +22,6 @@ dataSources: - kind: substrate/Runtime startBlock: 1 mapping: - file: ./dist/index.js handlers: - handler: handleBlock @@ -74,11 +73,31 @@ dataSources: filter: module: proxy method: AnonymousCreated - - handler: handleBorrowings + - handler: handleLoanCreated + kind: substrate/EventHandler + filter: + module: loans + method: Created + - handler: handleLoanBorrowed kind: substrate/EventHandler filter: module: loans method: Borrowed + - handler: handleLoanPriced + kind: substrate/EventHandler + filter: + module: loans + method: Priced + - handler: handleLoanRepaid + kind: substrate/EventHandler + filter: + module: loans + method: Repaid + - handler: handleLoanClosed + kind: substrate/EventHandler + filter: + module: loans + method: Closed - handler: handleTokenTransfer kind: substrate/EventHandler filter: diff --git a/schema.graphql b/schema.graphql index dbd1922a..4fa11622 100644 --- a/schema.graphql +++ b/schema.graphql @@ -252,12 +252,19 @@ type Loan @entity { # collateral: status: LoanStatus! - # TODO: how to store loan type data + + type: String + spec: String interestRatePerSec: BigInt outstandingDebt: BigInt + totalBorrowed: BigInt + totalRepaid: BigInt + writeOffIndex: Int + writtenOffPercentage: BigInt + penaltyInterestRatePerSec: BigInt adminWrittenOff: Boolean pool: Pool! diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 4990d434..daf74a60 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -112,14 +112,26 @@ export interface AssetMetadata extends Struct { existentialDeposit: u128 } -export type LoanAsset = ITuple<[u64, u128]> +export interface LoanSpecs extends Struct { + advanceRate: u128 + value: u128 + probabilityOfDefault?: u128 + lossGivenDefault?: u128 + discountRate?: u128 + maturityDate?: u64 +} +export type LoanAsset = ITuple<[u64, u128]> export type PoolEvent = ITuple<[u64]> // poolId, loanId, collateral export type LoanCreatedClosedEvent = ITuple<[u64, u128, LoanAsset]> // poolId, loanId, amount -export type LoanBorrowedEvent = ITuple<[u64, u128, u128]> +export type LoanBorrowedRepaidEvent = ITuple<[u64, u128, u128]> +//poolId, loanId, interestRatePerSec, loanType +export type LoanPricedEvent = ITuple<[u64, u128, u128, Enum]> +//poolId, loanId, percentage, penaltyInterestRatePerSec, writeOffGroupIndex +export type LoanWrittenOffEvent = ITuple<[u64, u128, u128, u128, Option]> export type EpochEvent = ITuple<[u64, u32]> export type EpochSolutionEvent = ITuple<[u64, u32, EpochSolution]> diff --git a/src/mappings/handlers/loansHandlers.ts b/src/mappings/handlers/loansHandlers.ts index 5c3cc2b6..f9f8bbe3 100644 --- a/src/mappings/handlers/loansHandlers.ts +++ b/src/mappings/handlers/loansHandlers.ts @@ -1,13 +1,74 @@ import { SubstrateEvent } from '@subql/types' -import { LoanBorrowedEvent } from '../../helpers/types' +import { + LoanBorrowedRepaidEvent, + LoanCreatedClosedEvent, + LoanPricedEvent, + LoanWrittenOffEvent, +} from '../../helpers/types' import { errorHandler } from '../../helpers/errorHandler' import { PoolService } from '../services/poolService' +import { LoanService } from '../services/loanService' -export const handleBorrowings = errorHandler(_handleBorrowings) -async function _handleBorrowings(event: SubstrateEvent): Promise { - const [poolId, , amount] = event.event.data - logger.info(`Pool: ${poolId.toString()} borrowed ${amount.toString()}`) +export const handleLoanCreated = errorHandler(_handleLoanCreated) +async function _handleLoanCreated(event: SubstrateEvent) { + const [poolId, loanId] = event.event.data + logger.info(`Loan created event for pool: ${poolId.toString()} loan: ${loanId.toString()}`) + + const loan = await LoanService.init(poolId.toString(), loanId.toString(), event.block.timestamp) + await loan.save() +} + +export const handleLoanBorrowed = errorHandler(_handleLoanBorrowed) +async function _handleLoanBorrowed(event: SubstrateEvent): Promise { + const [poolId, loanId, amount] = event.event.data + logger.info(`Loan borrowed event for pool: ${poolId.toString()} amount: ${amount.toString()}`) + + // Update loan amount + const loan = await LoanService.getById(poolId.toString(), loanId.toString()) + await loan.borrow(amount.toBigInt()) + await loan.save() + + // Update poolState info const poolService = await PoolService.getById(poolId.toString()) await poolService.increaseTotalBorrowings(amount.toBigInt()) await poolService.save() } + +export const handleLoanPriced = errorHandler(_handleLoanPriced) +async function _handleLoanPriced(event: SubstrateEvent) { + const [poolId, loanId, interestRatePerSec, loanType] = event.event.data + logger.info(`Loan priced event for pool: ${poolId.toString()} loan: ${loanId.toString()}`) + const loan = await LoanService.getById(poolId.toString(), loanId.toString()) + await loan.activate() + await loan.updateInterestRate(interestRatePerSec.toBigInt()) + await loan.updateLoanType(loanType.type, loanType.inner.toJSON()) + await loan.save() +} + +export const handleLoanRepaid = errorHandler(_handleLoanRepaid) +async function _handleLoanRepaid(event: SubstrateEvent) { + const [poolId, loanId, amount] = event.event.data + logger.info(`Loan borrowed event for pool: ${poolId.toString()} amount: ${amount.toString()}`) + const loan = await LoanService.getById(poolId.toString(), loanId.toString()) + await loan.repay(amount.toBigInt()) + await loan.save() +} + +export const handleLoanWrittenOff = errorHandler(_handleLoanWrittenOff) +async function _handleLoanWrittenOff(event: SubstrateEvent) { + const [poolId, loanId, percentage, penaltyInterestRatePerSec, writeOffGroupIndex] = event.event.data + logger.info(`Loan writtenoff event for pool: ${poolId.toString()} loanId: ${loanId.toString()}`) + const loan = await LoanService.getById(poolId.toString(), loanId.toString()) + const writeOffIndex = writeOffGroupIndex.isSome ? writeOffGroupIndex.unwrap().toNumber() : null + await loan.writeOff(percentage.toBigInt(), penaltyInterestRatePerSec.toBigInt(), writeOffIndex) + await loan.save() +} + +export const handleLoanClosed = errorHandler(_handleLoanClosed) +async function _handleLoanClosed(event: SubstrateEvent) { + const [poolId, loanId] = event.event.data + logger.info(`Loan closed event for pool: ${poolId.toString()} loanId: ${loanId.toString()}`) + const loan = await LoanService.getById(poolId.toString(), loanId.toString()) + await loan.close() + await loan.save() +} diff --git a/src/mappings/handlers/poolsHandlers.ts b/src/mappings/handlers/poolsHandlers.ts index 7bde6bdd..814aa48e 100644 --- a/src/mappings/handlers/poolsHandlers.ts +++ b/src/mappings/handlers/poolsHandlers.ts @@ -59,7 +59,7 @@ async function _handleEpochClosed(event: SubstrateEvent): Promise): Promise< block ${event.block.block.header.number.toString()}` ) - const epoch = await EpochService.getById(`${poolId.toString()}-${epochId.toString()}`) + const epoch = await EpochService.getById(poolId.toString(), epochId.toNumber()) await epoch.executeEpoch(event.block.timestamp) await epoch.save() @@ -95,7 +95,7 @@ async function _handleEpochExecuted(event: SubstrateEvent): Promise< // Compute and save aggregated order fulfillment const tranches = await TrancheService.getByPoolId(poolId.toString()) - const nextEpoch = await EpochService.getById(`${poolId.toString()}-${(epochId.toNumber() + 1).toString()}`) + const nextEpoch = await EpochService.getById(poolId.toString(), epochId.toNumber() + 1) for (const tranche of tranches) { const epochState = epoch.epochStates.find((epochState) => epochState.trancheId === tranche.tranche.trancheId) await tranche.updateSupply() @@ -212,12 +212,12 @@ async function _handleInvestOrderUpdated(event: SubstrateEvent): Pro await oo.save() // Update tranche outstanding total - const tranche = await TrancheService.getById(`${poolId.toString()}-${trancheId.toHex()}`) + const tranche = await TrancheService.getById(poolId.toString(), trancheId.toHex()) await tranche.updateOutstandingInvestOrders(newAmount.toBigInt(), oldAmount.toBigInt()) await tranche.save() // Update epochState outstanding total - const epoch = await EpochService.getById(`${poolId.toString()}-${pool.pool.currentEpoch}`) + const epoch = await EpochService.getById(poolId.toString(), pool.pool.currentEpoch) await epoch.updateOutstandingInvestOrders(trancheId.toHex(), newAmount.toBigInt(), oldAmount.toBigInt()) await epoch.save() } @@ -257,12 +257,12 @@ async function _handleRedeemOrderUpdated(event: SubstrateEvent): Pro await oo.save() // Update tranche outstanding total - const tranche = await TrancheService.getById(`${poolId.toString()}-${trancheId.toHex()}`) + const tranche = await TrancheService.getById(poolId.toString(), trancheId.toHex()) await tranche.updateOutstandingRedeemOrders(newAmount.toBigInt(), oldAmount.toBigInt()) await tranche.save() // Update epochState outstanding total - const epoch = await EpochService.getById(`${poolId.toString()}-${pool.pool.currentEpoch}`) + const epoch = await EpochService.getById(poolId.toString(), pool.pool.currentEpoch) await epoch.updateOutstandingRedeemOrders(trancheId.toHex(), newAmount.toBigInt(), oldAmount.toBigInt()) await epoch.save() } diff --git a/src/mappings/services/epochService.ts b/src/mappings/services/epochService.ts index c08315f8..486e9f4f 100644 --- a/src/mappings/services/epochService.ts +++ b/src/mappings/services/epochService.ts @@ -13,11 +13,11 @@ export class EpochService { this.epochStates = epochStates } - static init = async (poolId: string, epochId: number, trancheIds: string[], timestamp: Date) => { - logger.info(`Initialising epoch ${epochId} for pool ${poolId}`) - const epoch = new Epoch(`${poolId}-${epochId.toString()}`) + static init = async (poolId: string, epochNr: number, trancheIds: string[], timestamp: Date) => { + logger.info(`Initialising epoch ${epochNr} for pool ${poolId}`) + const epoch = new Epoch(`${poolId}-${epochNr.toString()}`) - epoch.index = epochId + epoch.index = epochNr epoch.poolId = poolId epoch.openedAt = timestamp @@ -26,7 +26,7 @@ export class EpochService { const epochStates: EpochState[] = [] for (const trancheId of trancheIds) { - const epochState = new EpochState(`${poolId}-${epochId}-${trancheId}`) + const epochState = new EpochState(`${poolId}-${epochNr}-${trancheId}`) epochState.epochId = epoch.id epochState.trancheId = trancheId epochState.outstandingInvestOrders = BigInt(0) @@ -36,10 +36,10 @@ export class EpochService { return new EpochService(epoch, epochStates) } - static getById = async (epochId: string) => { - const epoch = await Epoch.get(epochId) + static getById = async (poolId: string, epochNr: number) => { + const epoch = await Epoch.get(`${poolId}-${epochNr.toString()}`) if (epoch === undefined) return undefined - const epochStates = await EpochState.getByEpochId(epochId) + const epochStates = await EpochState.getByEpochId(`${poolId}-${epochNr.toString()}`) return new EpochService(epoch, epochStates) } diff --git a/src/mappings/services/loanService.ts b/src/mappings/services/loanService.ts new file mode 100644 index 00000000..670dc06f --- /dev/null +++ b/src/mappings/services/loanService.ts @@ -0,0 +1,73 @@ +import { AnyJson } from '@polkadot/types/types' +import { Loan, LoanStatus } from '../../types' + +export class LoanService { + readonly loan: Loan + + constructor(loan: Loan) { + this.loan = loan + } + + static init = async (poolId: string, loanId: string, timestamp: Date) => { + logger.info(`Initialising loan ${loanId} for pool ${poolId}`) + const loan = new Loan(`${poolId}-${loanId}`) + + loan.createdAt = timestamp + loan.poolId = poolId + loan.status = LoanStatus.CREATED + loan.outstandingDebt = BigInt(0) + loan.totalBorrowed = BigInt(0) + loan.totalRepaid = BigInt(0) + + return new LoanService(loan) + } + + static getById = async (poolId: string, loanId: string) => { + const loan = await Loan.get(`${poolId}-${loanId}`) + if (loan === undefined) return undefined + return new LoanService(loan) + } + + public save = async () => { + await this.loan.save() + } + + public borrow = (amount: bigint) => { + logger.info(`Increasing outstanding debt for loan ${this.loan.id} by ${amount}`) + this.loan.totalBorrowed = this.loan.totalBorrowed + amount + } + + public repay = (amount: bigint) => { + logger.info(`Decreasing outstanding debt for loan ${this.loan.id} by ${amount}`) + this.loan.outstandingDebt = this.loan.totalRepaid + amount + } + + public updateInterestRate = (interestRatePerSec: bigint) => { + logger.info(`Updating interest rate for loan ${this.loan.id} to ${interestRatePerSec}`) + this.loan.interestRatePerSec = interestRatePerSec + } + + public writeOff = (percentage: bigint, penaltyInterestRatePerSec: bigint, writeOffIndex: number) => { + logger.info(`Writing off loan ${this.loan.id} with ${percentage}`) + this.loan.writtenOffPercentage = percentage + this.loan.penaltyInterestRatePerSec = penaltyInterestRatePerSec + this.loan.writeOffIndex = writeOffIndex + } + + public updateLoanType = (loanType: string, loanSpec?: AnyJson) => { + logger.info(`Updating loan type for loan ${this.loan.id} to ${loanType}`) + this.loan.type = loanType + const specBuff = Buffer.from(JSON.stringify(loanSpec)) + this.loan.spec = specBuff.toString('base64') + } + + public activate = () => { + logger.info(`Activating loan ${this.loan.id}`) + this.loan.status = LoanStatus.ACTIVE + } + + public close = () => { + logger.info(`Closing loan ${this.loan.id}`) + this.loan.status = LoanStatus.CLOSED + } +} diff --git a/src/mappings/services/trancheService.ts b/src/mappings/services/trancheService.ts index 9752e810..120f9053 100644 --- a/src/mappings/services/trancheService.ts +++ b/src/mappings/services/trancheService.ts @@ -41,9 +41,9 @@ export class TrancheService { return new TrancheService(tranche, trancheState) } - static getById = async (trancheId: string) => { - const tranche = await Tranche.get(trancheId) - const trancheState = await TrancheState.get(trancheId) + static getById = async (poolId: string, trancheId: string) => { + const tranche = await Tranche.get(`${poolId}-${trancheId}`) + const trancheState = await TrancheState.get(`${poolId}-${trancheId}`) if (tranche === undefined || trancheState === undefined) return undefined return new TrancheService(tranche, trancheState) }