diff --git a/schema.graphql b/schema.graphql index 78aab96..26405e8 100644 --- a/schema.graphql +++ b/schema.graphql @@ -136,6 +136,8 @@ type App implements Ressource @entity { checksum: Bytes! mrenclave: Bytes! timestamp: BigInt! # last transfer + usageCount: BigInt! + lastUsageTimestamp: BigInt! usages: [Deal!]! @derivedFrom(field: "app") orders: [AppOrder!]! @derivedFrom(field: "app") transfers: [AppTransfer!]! @derivedFrom(field: "app") @@ -161,6 +163,8 @@ type Workerpool implements Ressource @entity { workerStakeRatio: BigInt! schedulerRewardRatio: BigInt! timestamp: BigInt! # last transfer + usageCount: BigInt! + lastUsageTimestamp: BigInt! usages: [Deal!]! @derivedFrom(field: "workerpool") orders: [WorkerpoolOrder!]! @derivedFrom(field: "workerpool") events: [WorkerpoolEvent!]! @derivedFrom(field: "workerpool") diff --git a/src/Modules/IexecPoco.ts b/src/Modules/IexecPoco.ts index c1b5e0b..823b138 100644 --- a/src/Modules/IexecPoco.ts +++ b/src/Modules/IexecPoco.ts @@ -47,6 +47,7 @@ import { createContributionID, createEventID, fetchAccount, + fetchApp, fetchApporder, fetchContribution, fetchDatasetorder, @@ -54,6 +55,7 @@ import { fetchProtocol, fetchRequestorder, fetchTask, + fetchWorkerpool, fetchWorkerpoolorder, logTransaction, toRLC, @@ -148,6 +150,22 @@ export function handleOrdersMatched(event: OrdersMatchedEvent): void { requestorder.requester = viewedDeal.requester.toHex(); requestorder.save(); + // Update app usage statistics + let app = fetchApp(deal.app); + if (app != null) { + app.usageCount = app.usageCount.plus(deal.botSize); + app.lastUsageTimestamp = event.block.timestamp; + app.save(); + } + + // Update workerpool usage statistics + let workerpool = fetchWorkerpool(deal.workerpool); + if (workerpool != null) { + workerpool.usageCount = workerpool.usageCount.plus(deal.botSize); + workerpool.lastUsageTimestamp = event.block.timestamp; + workerpool.save(); + } + let orderMatchedEvent = new OrdersMatched(createEventID(event)); orderMatchedEvent.transaction = logTransaction(event).id; orderMatchedEvent.timestamp = event.block.timestamp; diff --git a/src/Registries/Appregistry.ts b/src/Registries/Appregistry.ts index 8242423..d6a3db7 100644 --- a/src/Registries/Appregistry.ts +++ b/src/Registries/Appregistry.ts @@ -47,6 +47,8 @@ export function handleTransferApp(ev: TransferEvent): void { app.checksum = contract.m_appChecksum(); app.mrenclave = contract.m_appMREnclave(); app.timestamp = ev.block.timestamp; + app.usageCount = BigInt.zero(); + app.lastUsageTimestamp = BigInt.zero(); app.save(); let transfer = new AppTransfer(createEventID(ev)); diff --git a/src/Registries/Workerpoolregistry.ts b/src/Registries/Workerpoolregistry.ts index f9196f5..e8e9d44 100644 --- a/src/Registries/Workerpoolregistry.ts +++ b/src/Registries/Workerpoolregistry.ts @@ -47,6 +47,8 @@ export function handleTransferWorkerpool(ev: TransferEvent): void { workerpool.workerStakeRatio = contract.m_workerStakeRatioPolicy(); workerpool.schedulerRewardRatio = contract.m_schedulerRewardRatioPolicy(); workerpool.timestamp = ev.block.timestamp; + workerpool.usageCount = BigInt.zero(); + workerpool.lastUsageTimestamp = BigInt.zero(); workerpool.save(); let transfer = new WorkerpoolTransfer(createEventID(ev)); diff --git a/src/utils.ts b/src/utils.ts index 91b7935..e7f43f6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,6 +13,7 @@ import { import { Account, + App, AppOrder, Bulk, BulkSlice, @@ -23,6 +24,7 @@ import { RequestOrder, Task, Transaction, + Workerpool, WorkerpoolOrder, } from '../generated/schema'; @@ -72,6 +74,14 @@ export function fetchAccount(id: string): Account { return account as Account; } +export function fetchApp(id: string): App | null { + return App.load(id); +} + +export function fetchWorkerpool(id: string): Workerpool | null { + return Workerpool.load(id); +} + export function fetchDeal(id: string): Deal { let deal = Deal.load(id); if (deal == null) { diff --git a/tests/unit/Modules/IexecPoco.test.ts b/tests/unit/Modules/IexecPoco.test.ts index d015064..cbabb2b 100644 --- a/tests/unit/Modules/IexecPoco.test.ts +++ b/tests/unit/Modules/IexecPoco.test.ts @@ -11,6 +11,7 @@ import { test, } from 'matchstick-as/assembly/index'; import { OrdersMatched } from '../../../generated/Core/IexecInterfaceToken'; +import { App, Workerpool } from '../../../generated/schema'; import { handleOrdersMatched } from '../../../src/Modules'; import { toRLC } from '../../../src/utils'; import { EventParamBuilder } from '../utils/EventParamBuilder'; @@ -52,6 +53,29 @@ describe('IexecPoco', () => { }); test('Should handle OrdersMatched', () => { + // Create app and workerpool entities first (they should exist from registry) + let app = new App(appAddress.toHex()); + app.owner = appOwner.toHex(); + app.name = 'TestApp'; + app.type = 'DOCKER'; + app.multiaddr = mockBytes32('multiaddr'); + app.checksum = mockBytes32('checksum'); + app.mrenclave = mockBytes32('mrenclave'); + app.timestamp = BigInt.zero(); + app.usageCount = BigInt.zero(); + app.lastUsageTimestamp = BigInt.zero(); + app.save(); + + let workerpool = new Workerpool(workerpoolAddress.toHex()); + workerpool.owner = workerpoolOwner.toHex(); + workerpool.description = 'TestWorkerpool'; + workerpool.workerStakeRatio = BigInt.fromI32(50); + workerpool.schedulerRewardRatio = BigInt.fromI32(10); + workerpool.timestamp = BigInt.zero(); + workerpool.usageCount = BigInt.zero(); + workerpool.lastUsageTimestamp = BigInt.zero(); + workerpool.save(); + mockViewDeal(pocoAddress, dealId).returns([buildDefaultDeal()]); // Create the mock event let mockEvent = newTypedMockEventWithParams( @@ -115,6 +139,173 @@ describe('IexecPoco', () => { // Assert that a transaction was logged (if applicable) const transactionId = mockEvent.transaction.hash.toHex(); assert.fieldEquals('Transaction', transactionId, 'id', transactionId); + + // Assert that app usage statistics were updated + assert.fieldEquals('App', appAddress.toHex(), 'usageCount', botSize.toString()); + assert.fieldEquals('App', appAddress.toHex(), 'lastUsageTimestamp', timestamp.toString()); + + // Assert that workerpool usage statistics were updated + assert.fieldEquals( + 'Workerpool', + workerpoolAddress.toHex(), + 'usageCount', + botSize.toString(), + ); + assert.fieldEquals( + 'Workerpool', + workerpoolAddress.toHex(), + 'lastUsageTimestamp', + timestamp.toString(), + ); + }); + + test('Should accumulate usage counts for multiple deals', () => { + const dealId1 = mockBytes32('dealId1'); + const dealId2 = mockBytes32('dealId2'); + const timestamp1 = BigInt.fromI32(123456789); + const timestamp2 = BigInt.fromI32(123456999); + const botSize1 = BigInt.fromI32(5); + const botSize2 = BigInt.fromI32(3); + + // Create app and workerpool entities first (they should exist from registry) + let app = new App(appAddress.toHex()); + app.owner = appOwner.toHex(); + app.name = 'TestApp'; + app.type = 'DOCKER'; + app.multiaddr = mockBytes32('multiaddr'); + app.checksum = mockBytes32('checksum'); + app.mrenclave = mockBytes32('mrenclave'); + app.timestamp = BigInt.zero(); + app.usageCount = BigInt.zero(); + app.lastUsageTimestamp = BigInt.zero(); + app.save(); + + let workerpool = new Workerpool(workerpoolAddress.toHex()); + workerpool.owner = workerpoolOwner.toHex(); + workerpool.description = 'TestWorkerpool'; + workerpool.workerStakeRatio = BigInt.fromI32(50); + workerpool.schedulerRewardRatio = BigInt.fromI32(10); + workerpool.timestamp = BigInt.zero(); + workerpool.usageCount = BigInt.zero(); + workerpool.lastUsageTimestamp = BigInt.zero(); + workerpool.save(); + + // First deal + mockViewDeal(pocoAddress, dealId1).returns([ + buildDeal( + appAddress, + appOwner, + appPrice, + datasetAddress, + datasetOwner, + datasetPrice, + workerpoolAddress, + workerpoolOwner, + workerpoolPrice, + trust, + category, + tag, + requester, + beneficiary, + callback, + params, + startTime, + botFirst, + botSize1, + workerStake, + schedulerRewardRatio, + sponsor, + ), + ]); + + let mockEvent1 = newTypedMockEventWithParams( + EventParamBuilder.init() + .bytes('dealid', dealId1) + .bytes('appHash', appHash) + .bytes('datasetHash', datasetHash) + .bytes('workerpoolHash', workerpoolHash) + .bytes('requestHash', requestHash) + .build(), + ); + mockEvent1.block.timestamp = timestamp1; + mockEvent1.address = pocoAddress; + + handleOrdersMatched(mockEvent1); + + // Verify first deal usage + assert.fieldEquals('App', appAddress.toHex(), 'usageCount', botSize1.toString()); + assert.fieldEquals('App', appAddress.toHex(), 'lastUsageTimestamp', timestamp1.toString()); + assert.fieldEquals( + 'Workerpool', + workerpoolAddress.toHex(), + 'usageCount', + botSize1.toString(), + ); + assert.fieldEquals( + 'Workerpool', + workerpoolAddress.toHex(), + 'lastUsageTimestamp', + timestamp1.toString(), + ); + + // Second deal + mockViewDeal(pocoAddress, dealId2).returns([ + buildDeal( + appAddress, + appOwner, + appPrice, + datasetAddress, + datasetOwner, + datasetPrice, + workerpoolAddress, + workerpoolOwner, + workerpoolPrice, + trust, + category, + tag, + requester, + beneficiary, + callback, + params, + startTime, + botFirst, + botSize2, + workerStake, + schedulerRewardRatio, + sponsor, + ), + ]); + + let mockEvent2 = newTypedMockEventWithParams( + EventParamBuilder.init() + .bytes('dealid', dealId2) + .bytes('appHash', mockBytes32('appHash2')) + .bytes('datasetHash', mockBytes32('datasetHash2')) + .bytes('workerpoolHash', mockBytes32('workerpoolHash2')) + .bytes('requestHash', mockBytes32('requestHash2')) + .build(), + ); + mockEvent2.block.timestamp = timestamp2; + mockEvent2.address = pocoAddress; + + handleOrdersMatched(mockEvent2); + + // Verify accumulated usage + const totalUsage = botSize1.plus(botSize2); + assert.fieldEquals('App', appAddress.toHex(), 'usageCount', totalUsage.toString()); + assert.fieldEquals('App', appAddress.toHex(), 'lastUsageTimestamp', timestamp2.toString()); + assert.fieldEquals( + 'Workerpool', + workerpoolAddress.toHex(), + 'usageCount', + totalUsage.toString(), + ); + assert.fieldEquals( + 'Workerpool', + workerpoolAddress.toHex(), + 'lastUsageTimestamp', + timestamp2.toString(), + ); }); test('Should handle OrdersMatched with bulk_cid when no dataset', () => {