diff --git a/bulla-contracts/schema.graphql b/bulla-contracts/schema.graphql index a681c90..d959d2e 100644 --- a/bulla-contracts/schema.graphql +++ b/bulla-contracts/schema.graphql @@ -234,6 +234,21 @@ type InvoiceUnfactoredEvent implements IEventLog & IPoolTransaction @entity { claim: Claim! } +type InvoiceImpairedEvent implements IEventLog & IPoolTransaction @entity { + id: ID! + invoiceId: String! + eventName: String! + fundedAmount: BigInt! + impairAmount: BigInt! + blockNumber: BigInt! + transactionHash: Bytes! + logIndex: BigInt! + timestamp: BigInt! + poolAddress: Bytes! + priceAfterTransaction: BigInt! + claim: Claim! +} + type DepositMadeEvent implements IEventLog & IPoolTransaction @entity { id: ID! depositor: Bytes! #address @@ -428,6 +443,18 @@ type User @entity { factoringEvents: [IEventLog!]! } +type PoolPnl @entity { + id: ID! + address: Bytes! #address + pnlHistory: [PnlHistoryEntry!]! +} + +type PnlHistoryEntry @entity { + id: ID! + pnl: BigInt! + timestamp: BigInt! +} + type FactoringPricePerShare @entity { id: ID! address: Bytes! # address diff --git a/bulla-contracts/src/functions/BullaFactoring.ts b/bulla-contracts/src/functions/BullaFactoring.ts index f820881..d04d933 100644 --- a/bulla-contracts/src/functions/BullaFactoring.ts +++ b/bulla-contracts/src/functions/BullaFactoring.ts @@ -2,6 +2,7 @@ import { BigInt, ethereum } from "@graphprotocol/graph-ts"; import { DepositMadeEvent, InvoiceFundedEvent, + InvoiceImpairedEvent, InvoiceKickbackAmountSentEvent, InvoicePaidEvent, InvoiceUnfactoredEvent, @@ -11,6 +12,7 @@ import { Deposit, DepositMadeWithAttachment, InvoiceFunded, + InvoiceImpaired, InvoiceKickbackAmountSent, InvoicePaid, InvoiceUnfactored, @@ -56,3 +58,9 @@ export const getSharesRedeemedEventId = (event: ethereum.Event): string => { }; export const createSharesRedeemedEvent = (event: Withdraw): SharesRedeemedEvent => new SharesRedeemedEvent(getSharesRedeemedEventId(event)); + +export const getInvoiceImpairedEventId = (underlyingClaimId: BigInt, event: ethereum.Event): string => + "InvoiceImpaired-" + underlyingClaimId.toString() + "-" + event.transaction.hash.toHexString() + "-" + event.logIndex.toString(); + +export const createInvoiceImpairedEvent = (underlyingTokenId: BigInt, event: InvoiceImpaired): InvoiceImpairedEvent => + new InvoiceImpairedEvent(getInvoiceImpairedEventId(underlyingTokenId, event)); diff --git a/bulla-contracts/src/functions/common.ts b/bulla-contracts/src/functions/common.ts index 0c7d4a1..c505679 100644 --- a/bulla-contracts/src/functions/common.ts +++ b/bulla-contracts/src/functions/common.ts @@ -4,7 +4,17 @@ import { ClaimCreatedClaimAttachmentStruct } from "../../generated/BullaClaimERC import { ERC20 } from "../../generated/BullaClaimERC721/ERC20"; import { BullaManager as BullaManagerContract } from "../../generated/BullaManager/BullaManager"; import { LoanOfferedLoanOfferAttachmentStruct } from "../../generated/FrendLend/FrendLend"; -import { BullaManager, Token, User, FactoringPricePerShare, PriceHistoryEntry, HistoricalFactoringStatistics, FactoringStatisticsEntry } from "../../generated/schema"; +import { + BullaManager, + Token, + User, + FactoringPricePerShare, + PriceHistoryEntry, + HistoricalFactoringStatistics, + FactoringStatisticsEntry, + PoolPnl, + PnlHistoryEntry +} from "../../generated/schema"; import { BullaFactoring, DepositMadeWithAttachmentAttachmentStruct, SharesRedeemedWithAttachmentAttachmentStruct } from "../../generated/BullaFactoring/BullaFactoring"; import { BigInt } from "@graphprotocol/graph-ts"; @@ -188,3 +198,27 @@ export const getOrCreateHistoricalFactoringStatistics = (event: ethereum.Event): return historicalFactoringStatistics; }; + +export const getOrCreatePoolProfitAndLoss = (event: ethereum.Event, pnl: BigInt): PoolPnl => { + let poolPnl = PoolPnl.load(event.address.toHexString()); + + if (!poolPnl) { + poolPnl = new PoolPnl(event.address.toHexString()); + poolPnl.address = event.address; + poolPnl.pnlHistory = []; + } + + const pnlHistoryEntryId = poolPnl.id.concat("-").concat(event.block.timestamp.toString()); + const pnlHistoryEntry = new PnlHistoryEntry(pnlHistoryEntryId); + pnlHistoryEntry.timestamp = event.block.timestamp; + pnlHistoryEntry.pnl = pnl; + pnlHistoryEntry.save(); + + let updatedHistory = poolPnl.pnlHistory; + updatedHistory.push(pnlHistoryEntry.id); + poolPnl.pnlHistory = updatedHistory; + + poolPnl.save(); + + return poolPnl; +}; diff --git a/bulla-contracts/src/mappings/BullaFactoring.ts b/bulla-contracts/src/mappings/BullaFactoring.ts index f13dab9..b3f69b0 100644 --- a/bulla-contracts/src/mappings/BullaFactoring.ts +++ b/bulla-contracts/src/mappings/BullaFactoring.ts @@ -3,6 +3,7 @@ import { Deposit, DepositMadeWithAttachment, InvoiceFunded, + InvoiceImpaired, InvoiceKickbackAmountSent, InvoicePaid, InvoicePaid__Params, @@ -14,6 +15,7 @@ import { getClaim } from "../functions/BullaClaimERC721"; import { createDepositMadeEvent, createInvoiceFundedEvent, + createInvoiceImpairedEvent, createInvoiceKickbackAmountSentEvent, createInvoicePaidEvent, createInvoiceUnfactoredEvent, @@ -26,6 +28,7 @@ import { getIPFSHash_redeemWithAttachment, getLatestPrice, getOrCreateHistoricalFactoringStatistics, + getOrCreatePoolProfitAndLoss, getOrCreatePricePerShare, getOrCreateUser } from "../functions/common"; @@ -106,7 +109,6 @@ export function handleInvoicePaid(event: InvoicePaid): void { const ev: InvoicePaid__Params = event.params; const originatingClaimId = ev.invoiceId; - log.info("in handleInvoicePaid", []); const underlyingClaim = getClaim(originatingClaimId.toString()); const InvoicePaidEvent = createInvoicePaidEvent(originatingClaimId, event); @@ -121,6 +123,7 @@ export function handleInvoicePaid(event: InvoicePaid): void { const price_per_share = getOrCreatePricePerShare(event); const latestPrice = getLatestPrice(event); const historical_factoring_statistics = getOrCreateHistoricalFactoringStatistics(event); + const pool_pnl = getOrCreatePoolProfitAndLoss(event, ev.trueInterest); InvoicePaidEvent.eventName = "InvoicePaid"; InvoicePaidEvent.blockNumber = event.block.number; @@ -137,6 +140,7 @@ export function handleInvoicePaid(event: InvoicePaid): void { original_creditor.save(); price_per_share.save(); historical_factoring_statistics.save(); + pool_pnl.save(); } export function handleInvoiceUnfactored(event: InvoiceUnfactored): void { @@ -258,3 +262,37 @@ export function handleSharesRedeemedWithAttachment(event: SharesRedeemedWithAtta sharesRedeemedEvent.save(); } + +export function handleInvoiceImpaired(event: InvoiceImpaired): void { + const ev = event.params; + const originatingClaimId = ev.invoiceId; + + const underlyingClaim = getClaim(originatingClaimId.toString()); + + const InvoiceImpairedEvent = createInvoiceImpairedEvent(originatingClaimId, event); + + InvoiceImpairedEvent.invoiceId = underlyingClaim.id; + const price_per_share = getOrCreatePricePerShare(event); + const latestPrice = getLatestPrice(event); + const historical_factoring_statistics = getOrCreateHistoricalFactoringStatistics(event); + + InvoiceImpairedEvent.eventName = "InvoiceImpaired"; + InvoiceImpairedEvent.blockNumber = event.block.number; + InvoiceImpairedEvent.transactionHash = event.transaction.hash; + InvoiceImpairedEvent.logIndex = event.logIndex; + InvoiceImpairedEvent.fundedAmount = ev.lossAmount; + InvoiceImpairedEvent.impairAmount = ev.gainAmount; + InvoiceImpairedEvent.timestamp = event.block.timestamp; + InvoiceImpairedEvent.poolAddress = event.address; + InvoiceImpairedEvent.priceAfterTransaction = latestPrice; + InvoiceImpairedEvent.claim = underlyingClaim.id; + const lossAccrued = ev.lossAmount + .minus(underlyingClaim.paidAmount) + .minus(ev.gainAmount) + .neg(); + const pool_pnl = getOrCreatePoolProfitAndLoss(event, lossAccrued); + + InvoiceImpairedEvent.save(); + price_per_share.save(); + historical_factoring_statistics.save(); +} diff --git a/bulla-contracts/template.yaml b/bulla-contracts/template.yaml index 95e2bed..8b76447 100644 --- a/bulla-contracts/template.yaml +++ b/bulla-contracts/template.yaml @@ -269,4 +269,6 @@ dataSources: handler: handleSharesRedeemed - event: SharesRedeemedWithAttachment(indexed address,uint256,uint256,(bytes32,uint8,uint8)) handler: handleSharesRedeemedWithAttachment + - event: InvoiceImpaired(indexed uint256,uint256,uint256) + handler: handleInvoiceImpaired file: ./src/mappings/BullaFactoring.ts diff --git a/bulla-contracts/tests/BullaFactoring.test.ts b/bulla-contracts/tests/BullaFactoring.test.ts index efc37bd..10335b1 100644 --- a/bulla-contracts/tests/BullaFactoring.test.ts +++ b/bulla-contracts/tests/BullaFactoring.test.ts @@ -6,6 +6,7 @@ import { handleDepositMade, handleDepositMadeWithAttachment, handleInvoiceFunded, + handleInvoiceImpaired, handleInvoiceKickbackAmountSent, handleInvoicePaid, handleInvoiceUnfactored, @@ -28,6 +29,7 @@ import { newDepositMadeEvent, newDepositMadeWithAttachmentEvent, newInvoiceFundedEvent, + newInvoiceImpairedEvent, newInvoiceKickbackAmountSentEvent, newInvoicePaidEvent, newInvoiceUnfactoredEvent, @@ -37,6 +39,7 @@ import { import { getDepositMadeEventId, getInvoiceFundedEventId, + getInvoiceImpairedEventId, getInvoiceKickbackAmountSentEventId, getInvoicePaidEventId, getInvoiceUnfactoredEventId, @@ -47,6 +50,8 @@ import { FactoringPricePerShare, FactoringStatisticsEntry, HistoricalFactoringStatistics, + PnlHistoryEntry, + PoolPnl, PriceHistoryEntry, SharesRedeemedEvent } from "../generated/schema"; @@ -258,6 +263,32 @@ test("it handles BullaFactoring events", () => { log.info("✅ should attach IPFS hash to SharesRedeemed event", []); + const lossAmount = BigInt.fromI32(2000); + const gainAmount = BigInt.fromI32(50); + + const invoiceImpairedEvent = newInvoiceImpairedEvent(claimId, lossAmount, gainAmount); + invoiceImpairedEvent.block.timestamp = timestamp; + invoiceImpairedEvent.block.number = blockNum; + + handleInvoiceImpaired(invoiceImpairedEvent); + + const invoiceImpairedEventId = getInvoiceImpairedEventId(claimId, invoiceImpairedEvent); + assert.fieldEquals("InvoiceImpairedEvent", invoiceImpairedEventId, "invoiceId", invoiceImpairedEvent.params.invoiceId.toString()); + assert.fieldEquals("InvoiceImpairedEvent", invoiceImpairedEventId, "fundedAmount", invoiceImpairedEvent.params.lossAmount.toString()); + assert.fieldEquals("InvoiceImpairedEvent", invoiceImpairedEventId, "impairAmount", invoiceImpairedEvent.params.gainAmount.toString()); + assert.fieldEquals("InvoiceImpairedEvent", invoiceImpairedEventId, "poolAddress", MOCK_BULLA_FACTORING_ADDRESS.toHexString()); + assert.fieldEquals("InvoiceImpairedEvent", invoiceImpairedEventId, "claim", claimId.toString()); + + let poolPnl = PoolPnl.load(MOCK_BULLA_FACTORING_ADDRESS.toHexString()); + assert.assertNotNull(poolPnl); + + const pnlHistoryEntryId = poolPnl!.pnlHistory[0]; + const pnlHistoryEntry = PnlHistoryEntry.load(pnlHistoryEntryId); + assert.assertNotNull(pnlHistoryEntry); + assert.bigIntEquals(lossAmount.minus(gainAmount).neg(), pnlHistoryEntry!.pnl); + + log.info("✅ should create a InvoiceImpaired event", []); + afterEach(); }); @@ -286,6 +317,14 @@ test("it handles InvoicePaid event", () => { handleInvoicePaid(invoicePaidEvent); + let poolPnl = PoolPnl.load(MOCK_BULLA_FACTORING_ADDRESS.toHexString()); + assert.assertNotNull(poolPnl); + + const pnlHistoryEntryId = poolPnl!.pnlHistory[0]; + const pnlHistoryEntry = PnlHistoryEntry.load(pnlHistoryEntryId); + assert.assertNotNull(pnlHistoryEntry); + assert.bigIntEquals(trueInterest, pnlHistoryEntry!.pnl); + const invoicePaidEventId = getInvoicePaidEventId(claimId, invoicePaidEvent); assert.fieldEquals("InvoicePaidEvent", invoicePaidEventId, "invoiceId", invoicePaidEvent.params.invoiceId.toString()); assert.fieldEquals("InvoicePaidEvent", invoicePaidEventId, "fundedAmount", invoicePaidEvent.params.fundedAmountNet.toString()); diff --git a/bulla-contracts/tests/functions/BullaFactoring.testtools.ts b/bulla-contracts/tests/functions/BullaFactoring.testtools.ts index e06e24b..0fdcee8 100644 --- a/bulla-contracts/tests/functions/BullaFactoring.testtools.ts +++ b/bulla-contracts/tests/functions/BullaFactoring.testtools.ts @@ -5,6 +5,7 @@ import { DepositMade, DepositMadeWithAttachment, InvoiceFunded, + InvoiceImpaired, InvoiceKickbackAmountSent, InvoicePaid, InvoiceUnfactored, @@ -226,6 +227,28 @@ export function newSharesRedeemedWithAttachmentEvent(redeemer: Address, assets: return sharesRedeemedWithAttachmentEvent; } +export function newInvoiceImpairedEvent(originatingClaimId: BigInt, lossAmount: BigInt, gainAmount: BigInt): InvoiceImpaired { + const mockEvent = newMockEvent(); + const invoiceImpairedEvent = new InvoiceImpaired( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt + ); + + invoiceImpairedEvent.address = MOCK_BULLA_FACTORING_ADDRESS; + invoiceImpairedEvent.parameters = new Array(); + invoiceImpairedEvent.parameters.push(new ethereum.EventParam("invoiceId", ethereum.Value.fromUnsignedBigInt(originatingClaimId))); + invoiceImpairedEvent.parameters.push(new ethereum.EventParam("lossAmount", ethereum.Value.fromUnsignedBigInt(lossAmount))); + invoiceImpairedEvent.parameters.push(new ethereum.EventParam("gainAmount", ethereum.Value.fromUnsignedBigInt(gainAmount))); + + return invoiceImpairedEvent; +} + function createMultihashTuple(): ethereum.Tuple { const hash: Bytes = changetype(Bytes.fromHexString(MULTIHASH_BYTES)); const multihashArray: Array = [