From ed2e0410ac4726f50188970fc8359417012ae0cd Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Tue, 21 Apr 2026 14:54:42 +0200 Subject: [PATCH 01/19] feat: anon piece selection and retrieval --- apps/backend/.env.example | 3 + apps/backend/src/app.module.ts | 2 + apps/backend/src/config/app.config.ts | 16 ++ .../entities/job-schedule-state.entity.ts | 1 + .../src/database/entities/retrieval.entity.ts | 22 +- .../1762000000000-AddAnonRetrievalColumns.ts | 67 ++++++ apps/backend/src/jobs/job-queues.ts | 1 + apps/backend/src/jobs/jobs.module.ts | 2 + apps/backend/src/jobs/jobs.service.spec.ts | 116 ++++----- apps/backend/src/jobs/jobs.service.ts | 107 ++++++++- .../metrics-prometheus/check-metric-labels.ts | 2 +- .../check-metrics.service.ts | 63 +++++ .../metrics-prometheus.module.ts | 53 +++++ .../pdp-subgraph/pdp-subgraph.service.spec.ts | 151 ++++++++++++ .../src/pdp-subgraph/pdp-subgraph.service.ts | 166 ++++++++++++- apps/backend/src/pdp-subgraph/queries.ts | 40 ++++ apps/backend/src/pdp-subgraph/types.ts | 92 ++++++++ .../anon-piece-selector.service.spec.ts | 136 +++++++++++ .../anon-piece-selector.service.ts | 100 ++++++++ .../retrieval-anon/anon-retrieval.service.ts | 180 ++++++++++++++ .../retrieval-anon/car-validation.service.ts | 223 ++++++++++++++++++ .../retrieval-anon/piece-retrieval.service.ts | 165 +++++++++++++ .../retrieval-anon/retrieval-anon.module.ts | 27 +++ apps/backend/src/retrieval-anon/types.ts | 33 +++ 24 files changed, 1695 insertions(+), 73 deletions(-) create mode 100644 apps/backend/src/database/migrations/1762000000000-AddAnonRetrievalColumns.ts create mode 100644 apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts create mode 100644 apps/backend/src/retrieval-anon/anon-piece-selector.service.ts create mode 100644 apps/backend/src/retrieval-anon/anon-retrieval.service.ts create mode 100644 apps/backend/src/retrieval-anon/car-validation.service.ts create mode 100644 apps/backend/src/retrieval-anon/piece-retrieval.service.ts create mode 100644 apps/backend/src/retrieval-anon/retrieval-anon.module.ts create mode 100644 apps/backend/src/retrieval-anon/types.ts diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 6815a66f..416b5cd5 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -52,6 +52,9 @@ DEALBOT_MAINTENANCE_WINDOW_MINUTES=20 DEALS_PER_SP_PER_HOUR=2 DATASET_CREATIONS_PER_SP_PER_HOUR=1 RETRIEVALS_PER_SP_PER_HOUR=1 +RETRIEVALS_ANON_PER_SP_PER_HOUR= +ANON_RETRIEVAL_BLOCK_SAMPLE_COUNT=5 +METRICS_PER_HOUR=2 PG_BOSS_LOCAL_CONCURRENCY=20 JOB_SCHEDULER_POLL_SECONDS=300 JOB_WORKER_POLL_SECONDS=60 diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 29751324..961daa87 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { JobsModule } from "./jobs/jobs.module.js"; import { MetricsPrometheusModule } from "./metrics-prometheus/metrics-prometheus.module.js"; import { ProvidersModule } from "./providers/providers.module.js"; import { RetrievalModule } from "./retrieval/retrieval.module.js"; +import { RetrievalAnonModule } from "./retrieval-anon/retrieval-anon.module.js"; @Module({ imports: [ @@ -26,6 +27,7 @@ import { RetrievalModule } from "./retrieval/retrieval.module.js"; JobsModule, DealModule, RetrievalModule, + RetrievalAnonModule, DataSourceModule, ProvidersModule, ...(process.env.ENABLE_DEV_MODE === "true" ? [DevToolsModule] : []), diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts index a8010bab..92e01554 100644 --- a/apps/backend/src/config/app.config.ts +++ b/apps/backend/src/config/app.config.ts @@ -80,6 +80,7 @@ export const configValidationSchema = Joi.object({ DEALS_PER_SP_PER_HOUR: Joi.number().min(0.001).max(20).default(4), DATASET_CREATIONS_PER_SP_PER_HOUR: Joi.number().min(0.001).max(20).default(1), RETRIEVALS_PER_SP_PER_HOUR: Joi.number().min(0.001).max(20).default(2), + RETRIEVALS_ANON_PER_SP_PER_HOUR: Joi.number().min(0.001).max(20).optional(), // Polling interval for pg-boss scheduler (lower = more responsive, higher = less DB chatter). JOB_SCHEDULER_POLL_SECONDS: Joi.number().min(60).default(300), JOB_WORKER_POLL_SECONDS: Joi.number().min(5).default(60), @@ -93,6 +94,7 @@ export const configValidationSchema = Joi.object({ RETRIEVAL_JOB_TIMEOUT_SECONDS: Joi.number().min(60).default(60), // 1 minute max runtime for retrieval jobs (TODO: reduce default to 30 seconds) DATA_SET_CREATION_JOB_TIMEOUT_SECONDS: Joi.number().min(60).default(300), // 5 minutes max runtime for dataset creation jobs IPFS_BLOCK_FETCH_CONCURRENCY: Joi.number().integer().min(1).max(32).default(6), + ANON_RETRIEVAL_BLOCK_SAMPLE_COUNT: Joi.number().integer().min(1).max(50).default(5), // Piece Cleanup MAX_DATASET_STORAGE_SIZE_BYTES: Joi.number() @@ -270,6 +272,12 @@ export interface IJobsConfig { * Only used when `DEALBOT_JOBS_MODE=pgboss`. */ maxPieceCleanupRuntimeSeconds: number; + + /** + * Target number of anonymous retrieval tests per storage provider per hour. + * Defaults to retrievalsPerSpPerHour when not set. + */ + retrievalsAnonPerSpPerHour: number; } export interface IDatasetConfig { @@ -287,6 +295,10 @@ export interface ITimeoutConfig { export interface IRetrievalConfig { ipfsBlockFetchConcurrency: number; + /** + * Number of CAR blocks to sample for IPNI + block-fetch validation. + */ + anonBlockSampleCount: number; } export interface IPieceCleanupConfig { @@ -381,6 +393,9 @@ export function loadConfig(): IConfig { enqueueJitterSeconds: Number.parseInt(process.env.JOB_ENQUEUE_JITTER_SECONDS || "0", 10), dealJobTimeoutSeconds: Number.parseInt(process.env.DEAL_JOB_TIMEOUT_SECONDS || "360", 10), retrievalJobTimeoutSeconds: Number.parseInt(process.env.RETRIEVAL_JOB_TIMEOUT_SECONDS || "60", 10), + retrievalsAnonPerSpPerHour: Number.parseFloat( + process.env.RETRIEVALS_ANON_PER_SP_PER_HOUR || process.env.RETRIEVALS_PER_SP_PER_HOUR || "2", + ), dataSetCreationJobTimeoutSeconds: Number.parseInt(process.env.DATA_SET_CREATION_JOB_TIMEOUT_SECONDS || "300", 10), pieceCleanupPerSpPerHour: Number.parseFloat(process.env.JOB_PIECE_CLEANUP_PER_SP_PER_HOUR || String(1 / 24)), maxPieceCleanupRuntimeSeconds: Number.parseInt(process.env.MAX_PIECE_CLEANUP_RUNTIME_SECONDS || "300", 10), @@ -412,6 +427,7 @@ export function loadConfig(): IConfig { }, retrieval: { ipfsBlockFetchConcurrency: Number.parseInt(process.env.IPFS_BLOCK_FETCH_CONCURRENCY || "6", 10), + anonBlockSampleCount: Number.parseInt(process.env.ANON_RETRIEVAL_BLOCK_SAMPLE_COUNT || "5", 10), }, pieceCleanup: { maxDatasetStorageSizeBytes: Number.parseInt( diff --git a/apps/backend/src/database/entities/job-schedule-state.entity.ts b/apps/backend/src/database/entities/job-schedule-state.entity.ts index d1758ae9..ebd5254d 100644 --- a/apps/backend/src/database/entities/job-schedule-state.entity.ts +++ b/apps/backend/src/database/entities/job-schedule-state.entity.ts @@ -6,6 +6,7 @@ import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, Update export type JobType = | "deal" | "retrieval" + | "retrieval_anon" | "data_set_creation" | "metrics" // legacy: no longer scheduled; see RemoveMetricsJobScheduleRows migration. TODO(#457): remove. | "metrics_cleanup" // legacy: no longer scheduled; see RemoveMetricsJobScheduleRows migration. TODO(#457): remove. diff --git a/apps/backend/src/database/entities/retrieval.entity.ts b/apps/backend/src/database/entities/retrieval.entity.ts index 18a59814..d72567c7 100644 --- a/apps/backend/src/database/entities/retrieval.entity.ts +++ b/apps/backend/src/database/entities/retrieval.entity.ts @@ -15,7 +15,7 @@ export class Retrieval { @PrimaryGeneratedColumn("uuid") id!: string; - @Column({ name: "deal_id", type: "uuid" }) + @Column({ name: "deal_id", type: "uuid", nullable: true }) dealId!: string; @Column({ @@ -63,6 +63,24 @@ export class Retrieval { @Column({ name: "retry_count", default: 0 }) retryCount!: number; + @Column({ name: "is_anonymous", type: "boolean", default: false }) + isAnonymous!: boolean; + + @Column({ name: "anon_piece_cid", nullable: true }) + anonPieceCid!: string; + + @Column({ name: "anon_data_set_id", nullable: true }) + anonDataSetId!: string; + + @Column({ name: "anon_piece_id", nullable: true }) + anonPieceId!: string; + + @Column({ name: "commp_valid", nullable: true }) + commPValid!: boolean; + + @Column({ name: "car_valid", nullable: true }) + carValid!: boolean; + @CreateDateColumn({ name: "created_at", type: "timestamptz" }) createdAt!: Date; @@ -70,7 +88,7 @@ export class Retrieval { updatedAt!: Date; // Relations - @ManyToOne("Deal", "retrievals", { onDelete: "CASCADE" }) + @ManyToOne("Deal", "retrievals", { onDelete: "CASCADE", nullable: true }) @JoinColumn({ name: "deal_id" }) deal: Deal | null; } diff --git a/apps/backend/src/database/migrations/1762000000000-AddAnonRetrievalColumns.ts b/apps/backend/src/database/migrations/1762000000000-AddAnonRetrievalColumns.ts new file mode 100644 index 00000000..34cb9c62 --- /dev/null +++ b/apps/backend/src/database/migrations/1762000000000-AddAnonRetrievalColumns.ts @@ -0,0 +1,67 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAnonRetrievalColumns1762000000000 implements MigrationInterface { + name = "AddAnonRetrievalColumns1762000000000"; + + public async up(queryRunner: QueryRunner): Promise { + // Make deal_id nullable — anonymous retrievals have no dealbot deal. + await queryRunner.query(` + ALTER TABLE retrievals + ALTER COLUMN deal_id DROP NOT NULL + `); + + await queryRunner.query(` + ALTER TABLE retrievals + ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN NOT NULL DEFAULT FALSE + `); + + await queryRunner.query(` + ALTER TABLE retrievals + ADD COLUMN IF NOT EXISTS anon_piece_cid TEXT DEFAULT NULL + `); + + await queryRunner.query(` + ALTER TABLE retrievals + ADD COLUMN IF NOT EXISTS anon_data_set_id TEXT DEFAULT NULL + `); + + await queryRunner.query(` + ALTER TABLE retrievals + ADD COLUMN IF NOT EXISTS anon_piece_id TEXT DEFAULT NULL + `); + + // NULL = not checked (e.g., retrieval failed before CommP step) + await queryRunner.query(` + ALTER TABLE retrievals + ADD COLUMN IF NOT EXISTS commp_valid BOOLEAN DEFAULT NULL + `); + + // NULL = not checked (e.g., withIPFSIndexing was false or piece retrieval failed) + await queryRunner.query(` + ALTER TABLE retrievals + ADD COLUMN IF NOT EXISTS car_valid BOOLEAN DEFAULT NULL + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_retrievals_is_anonymous" + ON retrievals (is_anonymous) + WHERE is_anonymous = TRUE + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_retrievals_is_anonymous"`); + await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS car_valid`); + await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS commp_valid`); + await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS anon_piece_id`); + await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS anon_data_set_id`); + await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS anon_piece_cid`); + await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS is_anonymous`); + + // Restore NOT NULL on deal_id (only safe if all anonymous rows are cleaned up first) + await queryRunner.query(` + ALTER TABLE retrievals + ALTER COLUMN deal_id SET NOT NULL + `); + } +} diff --git a/apps/backend/src/jobs/job-queues.ts b/apps/backend/src/jobs/job-queues.ts index 9488ce7b..db475d49 100644 --- a/apps/backend/src/jobs/job-queues.ts +++ b/apps/backend/src/jobs/job-queues.ts @@ -7,3 +7,4 @@ export const LEGACY_DEAL_QUEUE = "deal.run"; export const LEGACY_RETRIEVAL_QUEUE = "retrieval.run"; export const DATA_RETENTION_POLL_QUEUE = "data.retention.poll"; export const PROVIDERS_REFRESH_QUEUE = "providers.refresh"; +export const RETRIEVAL_ANON_QUEUE = "retrieval.anon.run"; diff --git a/apps/backend/src/jobs/jobs.module.ts b/apps/backend/src/jobs/jobs.module.ts index 15ad4d64..69f1edb1 100644 --- a/apps/backend/src/jobs/jobs.module.ts +++ b/apps/backend/src/jobs/jobs.module.ts @@ -7,6 +7,7 @@ import { StorageProvider } from "../database/entities/storage-provider.entity.js import { DealModule } from "../deal/deal.module.js"; import { PieceCleanupModule } from "../piece-cleanup/piece-cleanup.module.js"; import { RetrievalModule } from "../retrieval/retrieval.module.js"; +import { RetrievalAnonModule } from "../retrieval-anon/retrieval-anon.module.js"; import { WalletSdkModule } from "../wallet-sdk/wallet-sdk.module.js"; import { JobsService } from "./jobs.service.js"; import { JobScheduleRepository } from "./repositories/job-schedule.repository.js"; @@ -17,6 +18,7 @@ import { JobScheduleRepository } from "./repositories/job-schedule.repository.js TypeOrmModule.forFeature([StorageProvider, JobScheduleState]), DealModule, RetrievalModule, + RetrievalAnonModule, WalletSdkModule, DataRetentionModule, PieceCleanupModule, diff --git a/apps/backend/src/jobs/jobs.service.spec.ts b/apps/backend/src/jobs/jobs.service.spec.ts index 5b8c58bc..1206df17 100644 --- a/apps/backend/src/jobs/jobs.service.spec.ts +++ b/apps/backend/src/jobs/jobs.service.spec.ts @@ -29,18 +29,18 @@ describe("JobsService schedule rows", () => { }; let dataRetentionServiceMock: { pollDataRetention: ReturnType }; let metricsMocks: { - jobsQueuedGauge: JobsServiceDeps[8]; - jobsRetryScheduledGauge: JobsServiceDeps[9]; - oldestQueuedAgeGauge: JobsServiceDeps[10]; - oldestInFlightAgeGauge: JobsServiceDeps[11]; - jobsInFlightGauge: JobsServiceDeps[12]; - jobsEnqueueAttemptsCounter: JobsServiceDeps[13]; - jobsStartedCounter: JobsServiceDeps[14]; - jobsCompletedCounter: JobsServiceDeps[15]; - jobsPausedGauge: JobsServiceDeps[16]; - jobDuration: JobsServiceDeps[17]; - storageProvidersActive: JobsServiceDeps[18]; - storageProvidersTested: JobsServiceDeps[19]; + jobsQueuedGauge: JobsServiceDeps[9]; + jobsRetryScheduledGauge: JobsServiceDeps[10]; + oldestQueuedAgeGauge: JobsServiceDeps[11]; + oldestInFlightAgeGauge: JobsServiceDeps[12]; + jobsInFlightGauge: JobsServiceDeps[13]; + jobsEnqueueAttemptsCounter: JobsServiceDeps[14]; + jobsStartedCounter: JobsServiceDeps[15]; + jobsCompletedCounter: JobsServiceDeps[16]; + jobsPausedGauge: JobsServiceDeps[17]; + jobDuration: JobsServiceDeps[18]; + storageProvidersActive: JobsServiceDeps[19]; + storageProvidersTested: JobsServiceDeps[20]; }; let baseConfigValues: Partial; let configService: JobsServiceDeps[0]; @@ -51,21 +51,22 @@ describe("JobsService schedule rows", () => { jobScheduleRepository: JobsServiceDeps[2]; dealService: JobsServiceDeps[3]; retrievalService: JobsServiceDeps[4]; - walletSdkService: JobsServiceDeps[5]; - dataRetentionService: JobsServiceDeps[6]; - pieceCleanupService: JobsServiceDeps[7]; - jobsQueuedGauge: JobsServiceDeps[8]; - jobsRetryScheduledGauge: JobsServiceDeps[9]; - oldestQueuedAgeGauge: JobsServiceDeps[10]; - oldestInFlightAgeGauge: JobsServiceDeps[11]; - jobsInFlightGauge: JobsServiceDeps[12]; - jobsEnqueueAttemptsCounter: JobsServiceDeps[13]; - jobsStartedCounter: JobsServiceDeps[14]; - jobsCompletedCounter: JobsServiceDeps[15]; - jobsPausedGauge: JobsServiceDeps[16]; - jobDuration: JobsServiceDeps[17]; - storageProvidersActive: JobsServiceDeps[18]; - storageProvidersTested: JobsServiceDeps[19]; + anonRetrievalService: JobsServiceDeps[5]; + walletSdkService: JobsServiceDeps[6]; + dataRetentionService: JobsServiceDeps[7]; + pieceCleanupService: JobsServiceDeps[8]; + jobsQueuedGauge: JobsServiceDeps[9]; + jobsRetryScheduledGauge: JobsServiceDeps[10]; + oldestQueuedAgeGauge: JobsServiceDeps[11]; + oldestInFlightAgeGauge: JobsServiceDeps[12]; + jobsInFlightGauge: JobsServiceDeps[13]; + jobsEnqueueAttemptsCounter: JobsServiceDeps[14]; + jobsStartedCounter: JobsServiceDeps[15]; + jobsCompletedCounter: JobsServiceDeps[16]; + jobsPausedGauge: JobsServiceDeps[17]; + jobDuration: JobsServiceDeps[18]; + storageProvidersActive: JobsServiceDeps[19]; + storageProvidersTested: JobsServiceDeps[20]; }>, ) => JobsService; @@ -95,18 +96,18 @@ describe("JobsService schedule rows", () => { }; metricsMocks = { - jobsQueuedGauge: { set: vi.fn() } as unknown as JobsServiceDeps[8], - jobsRetryScheduledGauge: { set: vi.fn() } as unknown as JobsServiceDeps[9], - oldestQueuedAgeGauge: { set: vi.fn() } as unknown as JobsServiceDeps[10], - oldestInFlightAgeGauge: { set: vi.fn() } as unknown as JobsServiceDeps[11], - jobsInFlightGauge: { set: vi.fn() } as unknown as JobsServiceDeps[12], - jobsEnqueueAttemptsCounter: { inc: vi.fn() } as unknown as JobsServiceDeps[13], - jobsStartedCounter: { inc: vi.fn() } as unknown as JobsServiceDeps[14], - jobsCompletedCounter: { inc: vi.fn() } as unknown as JobsServiceDeps[15], - jobsPausedGauge: { set: vi.fn() } as unknown as JobsServiceDeps[16], - jobDuration: { observe: vi.fn() } as unknown as JobsServiceDeps[17], - storageProvidersActive: { set: vi.fn() } as unknown as JobsServiceDeps[18], - storageProvidersTested: { set: vi.fn() } as unknown as JobsServiceDeps[19], + jobsQueuedGauge: { set: vi.fn() } as unknown as JobsServiceDeps[9], + jobsRetryScheduledGauge: { set: vi.fn() } as unknown as JobsServiceDeps[10], + oldestQueuedAgeGauge: { set: vi.fn() } as unknown as JobsServiceDeps[11], + oldestInFlightAgeGauge: { set: vi.fn() } as unknown as JobsServiceDeps[12], + jobsInFlightGauge: { set: vi.fn() } as unknown as JobsServiceDeps[13], + jobsEnqueueAttemptsCounter: { inc: vi.fn() } as unknown as JobsServiceDeps[14], + jobsStartedCounter: { inc: vi.fn() } as unknown as JobsServiceDeps[15], + jobsCompletedCounter: { inc: vi.fn() } as unknown as JobsServiceDeps[16], + jobsPausedGauge: { set: vi.fn() } as unknown as JobsServiceDeps[17], + jobDuration: { observe: vi.fn() } as unknown as JobsServiceDeps[18], + storageProvidersActive: { set: vi.fn() } as unknown as JobsServiceDeps[19], + storageProvidersTested: { set: vi.fn() } as unknown as JobsServiceDeps[20], }; const emptySpBlocklists: ISpBlocklistConfig = { @@ -132,6 +133,7 @@ describe("JobsService schedule rows", () => { dataSetCreationJobTimeoutSeconds: 300, pieceCleanupPerSpPerHour: 1, maxPieceCleanupRuntimeSeconds: 300, + retrievalsAnonPerSpPerHour: 2, } as IConfig["jobs"], database: { host: "localhost", @@ -157,9 +159,10 @@ describe("JobsService schedule rows", () => { overrides.jobScheduleRepository ?? (jobScheduleRepositoryMock as unknown as JobsServiceDeps[2]), overrides.dealService ?? ({} as JobsServiceDeps[3]), overrides.retrievalService ?? ({} as JobsServiceDeps[4]), - overrides.walletSdkService ?? ({} as JobsServiceDeps[5]), - overrides.dataRetentionService ?? (dataRetentionServiceMock as unknown as JobsServiceDeps[6]), - overrides.pieceCleanupService ?? ({} as JobsServiceDeps[7]), + overrides.anonRetrievalService ?? ({} as JobsServiceDeps[5]), + overrides.walletSdkService ?? ({} as JobsServiceDeps[6]), + overrides.dataRetentionService ?? (dataRetentionServiceMock as unknown as JobsServiceDeps[7]), + overrides.pieceCleanupService ?? ({} as JobsServiceDeps[8]), overrides.jobsQueuedGauge ?? metricsMocks.jobsQueuedGauge, overrides.jobsRetryScheduledGauge ?? metricsMocks.jobsRetryScheduledGauge, overrides.oldestQueuedAgeGauge ?? metricsMocks.oldestQueuedAgeGauge, @@ -283,7 +286,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); // Trigger the timeout immediately by using fake timers @@ -342,7 +345,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, retrievalService: retrievalService as unknown as ConstructorParameters[4], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); vi.useFakeTimers(); @@ -381,7 +384,7 @@ describe("JobsService schedule rows", () => { service = buildService({ retrievalService: retrievalService as unknown as ConstructorParameters[4], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleRetrievalJob", { @@ -421,7 +424,7 @@ describe("JobsService schedule rows", () => { service = buildService({ retrievalService: retrievalService as unknown as ConstructorParameters[4], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await expect( @@ -614,12 +617,13 @@ describe("JobsService schedule rows", () => { // Check upserts for providerB const upsertCalls = jobScheduleRepositoryMock.upsertSchedule.mock.calls; const upsertsForB = upsertCalls.filter((call) => call[1] === providerB.address); - expect(upsertsForB).toHaveLength(4); + expect(upsertsForB).toHaveLength(5); expect(upsertsForB.map((call) => call[0]).sort()).toEqual([ "data_set_creation", "deal", "piece_cleanup", "retrieval", + "retrieval_anon", ]); }); @@ -925,7 +929,7 @@ describe("JobsService schedule rows", () => { service = buildService({ dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDealJob", { @@ -1006,7 +1010,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDealJob", { @@ -1059,7 +1063,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDealJob", { @@ -1112,7 +1116,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDealJob", { @@ -1167,7 +1171,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDealJob", { @@ -1197,7 +1201,7 @@ describe("JobsService schedule rows", () => { service = buildService({ dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDataSetCreationJob", { @@ -1238,7 +1242,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDataSetCreationJob", { @@ -1278,7 +1282,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDataSetCreationJob", { @@ -1320,7 +1324,7 @@ describe("JobsService schedule rows", () => { service = buildService({ configService, dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], }); await callPrivate(service, "handleDataSetCreationJob", { diff --git a/apps/backend/src/jobs/jobs.service.ts b/apps/backend/src/jobs/jobs.service.ts index 01357225..3ff48bfb 100644 --- a/apps/backend/src/jobs/jobs.service.ts +++ b/apps/backend/src/jobs/jobs.service.ts @@ -15,18 +15,22 @@ import { StorageProvider } from "../database/entities/storage-provider.entity.js import { DealService } from "../deal/deal.service.js"; import { PieceCleanupService } from "../piece-cleanup/piece-cleanup.service.js"; import { RetrievalService } from "../retrieval/retrieval.service.js"; +import { AnonRetrievalService } from "../retrieval-anon/anon-retrieval.service.js"; import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; import { provisionNextMissingDataSet } from "./data-set-creation.handler.js"; -import { DATA_RETENTION_POLL_QUEUE, PROVIDERS_REFRESH_QUEUE, SP_WORK_QUEUE } from "./job-queues.js"; +import { DATA_RETENTION_POLL_QUEUE, PROVIDERS_REFRESH_QUEUE, RETRIEVAL_ANON_QUEUE,SP_WORK_QUEUE } from "./job-queues.js"; import { JobScheduleRepository } from "./repositories/job-schedule.repository.js"; -type SpJobType = "deal" | "retrieval" | "data_set_creation" | "piece_cleanup"; -const SP_JOB_TYPES: ReadonlySet = new Set(["deal", "retrieval", "data_set_creation", "piece_cleanup"]); +type SpJobType = "deal" | "retrieval" | "data_set_creation" | "retrieval_anon" | "piece_cleanup"; +const SP_JOB_TYPES: ReadonlySet = new Set(["deal", "retrieval", "retrieval_anon", "data_set_creation", "piece_cleanup"]); + function isSpJobType(jobType: string): jobType is SpJobType { return SP_JOB_TYPES.has(jobType); } type SpJobData = { jobType: SpJobType; spAddress: string; intervalSeconds: number }; +type AnonRetrievalJobData = { spAddress: string; intervalSeconds: number }; +type MetricsJobData = { intervalSeconds: number }; type ProvidersRefreshJobData = { intervalSeconds: number }; type SpJob = Job; type DataRetentionJobData = { intervalSeconds: number }; @@ -57,6 +61,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { private readonly jobScheduleRepository: JobScheduleRepository, private readonly dealService: DealService, private readonly retrievalService: RetrievalService, + private readonly anonRetrievalService: AnonRetrievalService, private readonly walletSdkService: WalletSdkService, private readonly dataRetentionService: DataRetentionService, private readonly pieceCleanupService: PieceCleanupService, @@ -257,6 +262,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { await boss.createQueue(SP_WORK_QUEUE, { policy: "singleton" }); await boss.createQueue(PROVIDERS_REFRESH_QUEUE); await boss.createQueue(DATA_RETENTION_POLL_QUEUE); + await boss.createQueue(RETRIEVAL_ANON_QUEUE); } private registerWorkers(): void { @@ -334,6 +340,23 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { error: toStructuredError(error), }), ); + void this.boss + .work( + RETRIEVAL_ANON_QUEUE, + { batchSize: 1, localConcurrency: spConcurrency, pollingIntervalSeconds: workerPollSeconds }, + async ([job]) => { + if (!job) return; + await this.handleAnonRetrievalJob(job); + }, + ) + .catch((error) => + this.logger.error({ + event: "worker_register_failed", + message: "Failed to register worker", + queue: RETRIEVAL_ANON_QUEUE, + error: toStructuredError(error), + }), + ); } private getMaintenanceWindowStatus(now: Date = new Date()) { @@ -621,6 +644,51 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { }); } + private async handleAnonRetrievalJob(job: Job): Promise { + const data = job.data; + const spAddress = data.spAddress; + + // Create AbortController for job timeout enforcement + const abortController = new AbortController(); + const timeoutSeconds = this.configService.get("jobs").retrievalJobTimeoutSeconds; + const timeoutMs = Math.max(60000, timeoutSeconds * 1000); + const effectiveTimeoutSeconds = Math.round(timeoutMs / 1000); + const abortReason = new Error(`Anon retrieval job timeout (${effectiveTimeoutSeconds}s) for ${spAddress}`); + const timeoutId = setTimeout(() => { + abortController.abort(abortReason); + }, timeoutMs); + + await this.recordJobExecution("retrieval_anon", async () => { + const logContext = await this.resolveProviderJobContext(spAddress, job.id); + try { + await this.anonRetrievalService.performForProvider(spAddress, abortController.signal, logContext); + return "success"; + } catch (error) { + if (abortController.signal.aborted) { + const reason = abortController.signal.reason; + const reasonMessage = reason instanceof Error ? reason.message : String(reason ?? ""); + this.logger.error({ + ...logContext, + event: "anon_retrieval_job_aborted", + message: reasonMessage || "Anon retrieval job aborted after timeout", + timeoutSeconds: effectiveTimeoutSeconds, + error: toStructuredError(reason ?? error), + }); + return "aborted"; + } + this.logger.error({ + ...logContext, + event: "anon_retrieval_job_failed", + message: "Anon retrieval job failed", + error: toStructuredError(error), + }); + throw error; + } finally { + clearTimeout(timeoutId); + } + }); + } + private async handleDataRetentionJob(data: DataRetentionJobData): Promise { void data; await this.recordJobExecution("data_retention_poll", async () => { @@ -838,7 +906,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { private async deferJobForMaintenance( jobType: SpJobType, - data: SpJobData, + data: SpJobData | AnonRetrievalJobData, maintenance: ReturnType, now: Date, ): Promise { @@ -846,7 +914,8 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { if (resumeAt == null) { return; } - await this.safeSend(jobType, SP_WORK_QUEUE, data, { startAfter: resumeAt }); + const queueName = jobType === "retrieval_anon" ? RETRIEVAL_ANON_QUEUE : SP_WORK_QUEUE; + await this.safeSend(jobType, queueName, data, { startAfter: resumeAt }); } /** @@ -899,6 +968,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { private getIntervalSecondsForRates(): { dealIntervalSeconds: number; retrievalIntervalSeconds: number; + retrievalAnonIntervalSeconds: number; dataSetCreationIntervalSeconds: number; dataRetentionPollIntervalSeconds: number; providersRefreshIntervalSeconds: number; @@ -919,9 +989,13 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { const dataRetentionPollIntervalSeconds = scheduling.dataRetentionPollIntervalSeconds; const providersRefreshIntervalSeconds = scheduling.providersRefreshIntervalSeconds; + const retrievalsAnonPerHour = jobsConfig.retrievalsAnonPerSpPerHour; + const retrievalAnonIntervalSeconds = Math.max(1, Math.round(3600 / retrievalsAnonPerHour)); + return { dealIntervalSeconds, retrievalIntervalSeconds, + retrievalAnonIntervalSeconds, dataSetCreationIntervalSeconds, dataRetentionPollIntervalSeconds, providersRefreshIntervalSeconds, @@ -941,6 +1015,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { const { dealIntervalSeconds, retrievalIntervalSeconds, + retrievalAnonIntervalSeconds, dataSetCreationIntervalSeconds, dataRetentionPollIntervalSeconds, providersRefreshIntervalSeconds, @@ -958,6 +1033,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { const phaseMs = this.schedulePhaseSeconds() * 1000; const dealStartAt = new Date(now.getTime() + phaseMs); const retrievalStartAt = new Date(now.getTime() + phaseMs); + const retrievalAnonStartAt = new Date(now.getTime() + phaseMs); const dataSetCreationStartAt = new Date(now.getTime() + phaseMs); const dataRetentionPollStartAt = new Date(now.getTime() + phaseMs); const providersRefreshStartAt = new Date(now.getTime() + phaseMs); @@ -981,6 +1057,12 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { for (const address of unblockedAddresses) { await this.jobScheduleRepository.upsertSchedule("deal", address, dealIntervalSeconds, dealStartAt); await this.jobScheduleRepository.upsertSchedule("retrieval", address, retrievalIntervalSeconds, retrievalStartAt); + await this.jobScheduleRepository.upsertSchedule( + "retrieval_anon", + address, + retrievalAnonIntervalSeconds, + retrievalAnonStartAt, + ); if (minDataSets >= 1) { await this.jobScheduleRepository.upsertSchedule( "data_set_creation", @@ -1138,6 +1220,8 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { return SP_WORK_QUEUE; case "piece_cleanup": return SP_WORK_QUEUE; + case "retrieval_anon": + return RETRIEVAL_ANON_QUEUE; case "data_retention_poll": return DATA_RETENTION_POLL_QUEUE; case "providers_refresh": @@ -1154,14 +1238,12 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { } private mapJobPayload(row: ScheduleRow): SpJobData | ProvidersRefreshJobData | DataRetentionJobData { - if ( - row.job_type === "deal" || - row.job_type === "retrieval" || - row.job_type === "data_set_creation" || - row.job_type === "piece_cleanup" - ) { + if (row.job_type === "deal" || row.job_type === "retrieval" || row.job_type === "data_set_creation" || row.job_type === "piece_cleanup") { return { jobType: row.job_type, spAddress: row.sp_address, intervalSeconds: row.interval_seconds }; } + if (row.job_type === "retrieval_anon") { + return { spAddress: row.sp_address, intervalSeconds: row.interval_seconds }; + } return { intervalSeconds: row.interval_seconds }; } @@ -1179,7 +1261,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { // Disable retries so "attempted" jobs don't rerun; failures are handled by the next schedule tick. const finalOptions: SendOptions = { retryLimit: 0, ...options }; if (isSpJobType(jobType)) { - const spData = data as SpJobData; + const spData = data as { spAddress: string }; if (!finalOptions.singletonKey) { finalOptions.singletonKey = spData.spAddress; } @@ -1229,6 +1311,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { const jobTypes: JobType[] = [ "deal", "retrieval", + "retrieval_anon", "data_set_creation", "piece_cleanup", "data_retention_poll", diff --git a/apps/backend/src/metrics-prometheus/check-metric-labels.ts b/apps/backend/src/metrics-prometheus/check-metric-labels.ts index d8447160..9d776586 100644 --- a/apps/backend/src/metrics-prometheus/check-metric-labels.ts +++ b/apps/backend/src/metrics-prometheus/check-metric-labels.ts @@ -1,4 +1,4 @@ -export type CheckType = "dataStorage" | "retrieval" | "dataRetention" | "dataSetCreation"; +export type CheckType = "dataStorage" | "retrieval" | "anon_retrieval" | "dataRetention" | "dataSetCreation"; export type ProviderStatus = "approved" | "unapproved"; export type CheckMetricLabels = { diff --git a/apps/backend/src/metrics-prometheus/check-metrics.service.ts b/apps/backend/src/metrics-prometheus/check-metrics.service.ts index 55975cad..85f1cdcf 100644 --- a/apps/backend/src/metrics-prometheus/check-metrics.service.ts +++ b/apps/backend/src/metrics-prometheus/check-metrics.service.ts @@ -248,3 +248,66 @@ export class DataSetCreationCheckMetrics { this.dataSetCreationStatusCounter.inc({ ...labels, value }); } } + +@Injectable() +export class AnonRetrievalCheckMetrics { + constructor( + @InjectMetric("anonPieceRetrievalFirstByteMs") + private readonly firstByteMs: Histogram, + @InjectMetric("anonPieceRetrievalLastByteMs") + private readonly lastByteMs: Histogram, + @InjectMetric("anonPieceRetrievalThroughputBps") + private readonly throughputBps: Histogram, + @InjectMetric("anonRetrievalCheckMs") + private readonly checkMs: Histogram, + @InjectMetric("anonRetrievalStatus") + private readonly statusCounter: Counter, + @InjectMetric("anonPieceHttpResponseCode") + private readonly httpResponseCounter: Counter, + @InjectMetric("anonCarParseStatus") + private readonly carParseCounter: Counter, + @InjectMetric("anonIpniStatus") + private readonly ipniCounter: Counter, + @InjectMetric("anonBlockFetchStatus") + private readonly blockFetchCounter: Counter, + ) {} + + observeFirstByteMs(labels: CheckMetricLabels, value: number | null | undefined): void { + observePositive(this.firstByteMs, labels, value); + } + + observeLastByteMs(labels: CheckMetricLabels, value: number | null | undefined): void { + observePositive(this.lastByteMs, labels, value); + } + + observeThroughput(labels: CheckMetricLabels, value: number | null | undefined): void { + observePositive(this.throughputBps, labels, value); + } + + observeCheckDuration(labels: CheckMetricLabels, value: number | null | undefined): void { + observePositive(this.checkMs, labels, value); + } + + recordStatus(labels: CheckMetricLabels, value: string): void { + this.statusCounter.inc({ ...labels, value }); + } + + recordHttpResponseCode(labels: CheckMetricLabels, statusCode: number): void { + this.httpResponseCounter.inc({ + ...labels, + value: classifyHttpResponseCode(statusCode), + }); + } + + recordCarParseStatus(labels: CheckMetricLabels, parseable: boolean): void { + this.carParseCounter.inc({ ...labels, value: parseable ? "parseable" : "not_parseable" }); + } + + recordIpniStatus(labels: CheckMetricLabels, value: "valid" | "invalid" | "skipped"): void { + this.ipniCounter.inc({ ...labels, value }); + } + + recordBlockFetchStatus(labels: CheckMetricLabels, value: "valid" | "invalid" | "skipped"): void { + this.blockFetchCounter.inc({ ...labels, value }); + } +} diff --git a/apps/backend/src/metrics-prometheus/metrics-prometheus.module.ts b/apps/backend/src/metrics-prometheus/metrics-prometheus.module.ts index 18bda30d..45f728b6 100644 --- a/apps/backend/src/metrics-prometheus/metrics-prometheus.module.ts +++ b/apps/backend/src/metrics-prometheus/metrics-prometheus.module.ts @@ -8,6 +8,7 @@ import { } from "@willsoto/nestjs-prometheus"; import { WalletSdkModule } from "../wallet-sdk/wallet-sdk.module.js"; import { + AnonRetrievalCheckMetrics, DataSetCreationCheckMetrics, DataStorageCheckMetrics, DiscoverabilityCheckMetrics, @@ -207,6 +208,56 @@ const metricProviders = [ help: "Estimated number of unrecorded overdue proving periods per provider. Resets to 0 when the subgraph catches up.", labelNames: ["checkType", "providerId", "providerName", "providerStatus"] as const, }), + // Anonymous Retrieval Metrics + makeHistogramProvider({ + name: "anonPieceRetrievalFirstByteMs", + help: "Time to first byte for anonymous piece retrievals via /piece/{cid} (ms)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus"] as const, + buckets: [1, 5, 10, 50, 100, 250, 500, 1000, 2000, 5000, 10000, 30000], + }), + makeHistogramProvider({ + name: "anonPieceRetrievalLastByteMs", + help: "Total time to retrieve an anonymous piece via /piece/{cid} (ms)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus"] as const, + buckets: [1, 5, 10, 50, 100, 250, 500, 1000, 2000, 5000, 10000, 30000, 60000, 120000, 300000], + }), + makeHistogramProvider({ + name: "anonPieceRetrievalThroughputBps", + help: "Throughput for anonymous piece retrievals (bytes/s)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus"] as const, + buckets: throughputBuckets, + }), + makeHistogramProvider({ + name: "anonRetrievalCheckMs", + help: "End-to-end anonymous retrieval check duration (ms)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus"] as const, + buckets: [100, 500, 1000, 2000, 5000, 10000, 30000, 60000, 120000, 300000, 600000], + }), + makeCounterProvider({ + name: "anonRetrievalStatus", + help: "Anonymous retrieval overall outcome", + labelNames: ["checkType", "providerId", "providerName", "providerStatus", "value"] as const, + }), + makeCounterProvider({ + name: "anonPieceHttpResponseCode", + help: "HTTP response codes for anonymous piece retrieval requests", + labelNames: ["checkType", "providerId", "providerName", "providerStatus", "value"] as const, + }), + makeCounterProvider({ + name: "anonCarParseStatus", + help: "Anonymous retrieval CAR parse outcomes (parseable / not_parseable)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus", "value"] as const, + }), + makeCounterProvider({ + name: "anonIpniStatus", + help: "Anonymous retrieval IPNI check outcomes (valid / invalid / skipped)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus", "value"] as const, + }), + makeCounterProvider({ + name: "anonBlockFetchStatus", + help: "Anonymous retrieval block fetch validation outcomes (valid / invalid / skipped)", + labelNames: ["checkType", "providerId", "providerName", "providerStatus", "value"] as const, + }), // Storage provider metrics: absolute counts, independent of query filters. makeGaugeProvider({ name: "storage_providers_active", @@ -333,6 +384,7 @@ const metricProviders = [ RetrievalCheckMetrics, DiscoverabilityCheckMetrics, DataSetCreationCheckMetrics, + AnonRetrievalCheckMetrics, WalletBalanceCollector, // HTTP metrics interceptor { @@ -347,6 +399,7 @@ const metricProviders = [ RetrievalCheckMetrics, DiscoverabilityCheckMetrics, DataSetCreationCheckMetrics, + AnonRetrievalCheckMetrics, WalletBalanceCollector, ], }) diff --git a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts b/apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts index cd3a1ea8..56696dae 100644 --- a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts +++ b/apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts @@ -1,4 +1,5 @@ import type { ConfigService } from "@nestjs/config"; +import { CID } from "multiformats/cid"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { IConfig } from "../config/app.config.js"; import { PDPSubgraphService } from "./pdp-subgraph.service.js"; @@ -35,6 +36,33 @@ const makeSubgraphMetaResponse = (blockNumber = 12345) => ({ }, }); +const FWSS_SP_ADDRESS = "0xAaaaAAaaaaAAaaaAaAaAaaAaaaAaAaAaaAaaa111"; +const FWSS_PAYER = "0xBBbbBBbbBBbBBbBbbBBbbBBbbbbBbBBbbBBbb222"; +const EXAMPLE_PIECE_CID = "baga6ea4seaqpzwrimvoc4jp4l7mk6knsknf6owsc2ev4krrs2peenl5qelh6u4y"; +const pieceCidHex = `0x${Buffer.from(CID.parse(EXAMPLE_PIECE_CID).bytes).toString("hex")}`; + +const makeCandidateResponse = (dataSets: Record[] = [], blockNumber = 12345) => ({ + data: { + _meta: { block: { number: blockNumber } }, + dataSets, + }, +}); + +const makeFwssDataSet = (overrides: Record = {}) => ({ + setId: "42", + withIPFSIndexing: true, + pdpPaymentEndEpoch: null, + roots: [ + { + rootId: "1", + cid: pieceCidHex, + rawSize: "1048576", + ipfsRootCID: "bafyroot", + }, + ], + ...overrides, +}); + describe("PDPSubgraphService", () => { let service: PDPSubgraphService; let fetchMock: ReturnType; @@ -691,4 +719,127 @@ describe("PDPSubgraphService", () => { expect(timestamps.length).toBe(1); }); }); + + describe("listFwssCandidatePieces", () => { + it("returns empty array when endpoint is not configured", async () => { + const noEndpointConfig = { + get: vi.fn(() => ({ pdpSubgraphEndpoint: "" })), + } as unknown as ConfigService; + const noEndpointService = new PDPSubgraphService(noEndpointConfig); + + const pieces = await noEndpointService.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + expect(pieces).toEqual([]); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("parses datasets and returns decoded candidate pieces", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => makeCandidateResponse([makeFwssDataSet()]), + }); + + const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + + expect(pieces).toHaveLength(1); + expect(pieces[0]).toMatchObject({ + pieceCid: EXAMPLE_PIECE_CID, + pieceId: "1", + dataSetId: "42", + rawSize: "1048576", + withIPFSIndexing: true, + ipfsRootCid: "bafyroot", + }); + }); + + it("lowercases SP and payer addresses before querying", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => makeCandidateResponse([]), + }); + + await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + + const [, opts] = fetchMock.mock.calls[0]; + const body = JSON.parse(opts.body as string); + expect(body.variables.serviceProvider).toBe(FWSS_SP_ADDRESS.toLowerCase()); + expect(body.variables.payer).toBe(FWSS_PAYER.toLowerCase()); + }); + + it("filters out datasets whose pdpPaymentEndEpoch has already passed", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => + makeCandidateResponse( + [ + makeFwssDataSet({ setId: "10", pdpPaymentEndEpoch: "5000" }), + makeFwssDataSet({ setId: "11", pdpPaymentEndEpoch: "20000" }), + makeFwssDataSet({ setId: "12", pdpPaymentEndEpoch: null }), + ], + 10_000, + ), + }); + + const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + + const dataSetIds = pieces.map((p) => p.dataSetId).sort(); + expect(dataSetIds).toEqual(["11", "12"]); + }); + + it("skips pieces whose CID fails to decode but keeps valid ones", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => + makeCandidateResponse([ + makeFwssDataSet({ + roots: [ + { rootId: "1", cid: pieceCidHex, rawSize: "1024", ipfsRootCID: null }, + { rootId: "2", cid: "0xdeadbeef", rawSize: "2048", ipfsRootCID: null }, + ], + }), + ]), + }); + + const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + expect(pieces).toHaveLength(1); + expect(pieces[0].pieceId).toBe("1"); + }); + + it("propagates null ipfsRootCID through to the candidate piece", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => + makeCandidateResponse([ + makeFwssDataSet({ + withIPFSIndexing: false, + roots: [{ rootId: "1", cid: pieceCidHex, rawSize: "1024", ipfsRootCID: null }], + }), + ]), + }); + + const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + expect(pieces[0].ipfsRootCid).toBeNull(); + expect(pieces[0].withIPFSIndexing).toBe(false); + }); + + it("throws after max retries on repeated HTTP errors", async () => { + fetchMock.mockResolvedValue({ ok: false, status: 500, statusText: "Internal Server Error" }); + + const promise = service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + promise.catch(() => {}); + await vi.runAllTimersAsync(); + + await expect(promise).rejects.toThrow("Failed to fetch subgraph fwss_candidate_pieces after 3 attempts"); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("does not retry on schema validation failure", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ data: { _meta: { block: { number: 1 } } } }), // missing dataSets + }); + + await expect(service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER)).rejects.toThrow(/validation failed/i); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts b/apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts index aedd8bce..2b767de7 100644 --- a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts +++ b/apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts @@ -3,8 +3,20 @@ import { ConfigService } from "@nestjs/config"; import { toStructuredError } from "../common/logging.js"; import type { IBlockchainConfig, IConfig } from "../config/app.config.js"; import { Queries } from "./queries.js"; -import type { GraphQLResponse, ProviderDataSetResponse, ProvidersWithDataSetsOptions, SubgraphMeta } from "./types.js"; -import { validateProviderDataSetResponse, validateSubgraphMetaResponse } from "./types.js"; +import type { + FwssCandidatePiece, + GraphQLResponse, + ProviderDataSetResponse, + ProvidersWithDataSetsOptions, + RawCandidatePiecesResponse, + SubgraphMeta, +} from "./types.js"; +import { + decodePieceCid, + validateCandidatePiecesResponse, + validateProviderDataSetResponse, + validateSubgraphMetaResponse, +} from "./types.js"; /** * Error thrown when data validation fails. @@ -31,6 +43,11 @@ export class PDPSubgraphService { private static readonly MAX_RETRIES = 3; private static readonly INITIAL_RETRY_DELAY_MS = 1000; + /** Max active FWSS datasets fetched per SP per candidate-pieces query. */ + private static readonly FWSS_DATASET_LIMIT = 100; + /** Max pieces fetched per dataset in the candidate-pieces query. */ + private static readonly FWSS_PIECE_LIMIT = 50; + private requestTimestamps: number[] = []; constructor(private readonly configService: ConfigService) { @@ -143,6 +160,151 @@ export class PDPSubgraphService { return this.fetchMultipleBatchesWithRateLimit(blockNumber, addresses); } + /** + * List FWSS candidate pieces for anonymous retrieval testing against the given SP. + * + * Queries for active FWSS datasets owned by `spAddress`, excluding those paid for + * by `dealbotPayer` (the dealbot's own datasets). Datasets whose `pdpPaymentEndEpoch` + * has already passed the latest indexed block are filtered out client-side — the + * subgraph does not flip `isActive` for payment termination. + * + * Returns an empty array if the subgraph endpoint is not configured. + */ + async listFwssCandidatePieces(spAddress: string, dealbotPayer: string): Promise { + if (!this.blockchainConfig.pdpSubgraphEndpoint) { + return []; + } + + const variables = { + serviceProvider: spAddress.toLowerCase(), + payer: dealbotPayer.toLowerCase(), + datasetLimit: PDPSubgraphService.FWSS_DATASET_LIMIT, + pieceLimit: PDPSubgraphService.FWSS_PIECE_LIMIT, + }; + + const validated = await this.executeQuery( + "fwss_candidate_pieces", + Queries.GET_FWSS_CANDIDATE_PIECES, + variables, + validateCandidatePiecesResponse, + ); + + return this.toCandidatePieces(validated); + } + + private toCandidatePieces(response: RawCandidatePiecesResponse): FwssCandidatePiece[] { + const currentEpoch = BigInt(response._meta.block.number); + const pieces: FwssCandidatePiece[] = []; + + for (const ds of response.dataSets) { + if (ds.pdpPaymentEndEpoch != null && BigInt(ds.pdpPaymentEndEpoch) <= currentEpoch) { + continue; + } + + for (const r of ds.roots) { + try { + pieces.push({ + pieceCid: decodePieceCid(r.cid), + pieceId: r.rootId, + dataSetId: ds.setId, + rawSize: r.rawSize, + withIPFSIndexing: ds.withIPFSIndexing, + ipfsRootCid: r.ipfsRootCID ?? null, + }); + } catch (error) { + this.logger.warn({ + event: "fwss_piece_cid_decode_failed", + message: "Failed to decode piece CID from subgraph data", + dataSetId: ds.setId, + pieceId: r.rootId, + error: toStructuredError(error), + }); + } + } + } + + return pieces; + } + + /** + * Generic single-query helper with retry and rate limiting. Used by queries that + * don't fit the batched provider-fetch shape. + */ + private async executeQuery( + operationName: string, + query: string, + variables: Record, + transform: (data: unknown) => T, + attempt: number = 1, + ): Promise { + if (!this.blockchainConfig.pdpSubgraphEndpoint) { + throw new Error("No PDP subgraph endpoint configured"); + } + + try { + await this.enforceRateLimit(); + + const response = await fetch(this.blockchainConfig.pdpSubgraphEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = (await response.json()) as GraphQLResponse; + + if (result.errors) { + const errorMessage = result.errors?.[0]?.message || "Unknown GraphQL error"; + throw new Error(`GraphQL error: ${errorMessage}`); + } + + try { + return transform(result.data); + } catch (validationError) { + const errorMessage = validationError instanceof Error ? validationError.message : "Unknown validation error"; + throw new ValidationError(`Data validation failed: ${errorMessage}`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + + if (error instanceof ValidationError) { + this.logger.error({ + event: `subgraph_${operationName}_validation_failed`, + message: `Subgraph ${operationName} validation failed`, + error: toStructuredError(error), + }); + throw error; + } + + if (attempt < PDPSubgraphService.MAX_RETRIES) { + const delay = PDPSubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); + this.logger.warn({ + event: `subgraph_${operationName}_request_retry`, + message: `Subgraph ${operationName} request failed. Retrying...`, + attempt, + maxRetries: PDPSubgraphService.MAX_RETRIES, + retryDelayMs: delay, + error: toStructuredError(error), + }); + await new Promise((resolve) => setTimeout(resolve, delay)); + return this.executeQuery(operationName, query, variables, transform, attempt + 1); + } + + this.logger.error({ + event: `subgraph_${operationName}_request_failed`, + message: `Subgraph ${operationName} request failed after maximum retries`, + maxRetries: PDPSubgraphService.MAX_RETRIES, + error: toStructuredError(error), + }); + throw new Error( + `Failed to fetch subgraph ${operationName} after ${PDPSubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, + ); + } + } + /** * Fetch multiple batches with rate limiting and concurrency control */ diff --git a/apps/backend/src/pdp-subgraph/queries.ts b/apps/backend/src/pdp-subgraph/queries.ts index a21a3991..3f750da4 100644 --- a/apps/backend/src/pdp-subgraph/queries.ts +++ b/apps/backend/src/pdp-subgraph/queries.ts @@ -21,4 +21,44 @@ export const Queries = { } } `, + GET_FWSS_CANDIDATE_PIECES: ` + query GetFwssCandidatePieces( + $serviceProvider: Bytes! + $payer: Bytes! + $datasetLimit: Int! + $pieceLimit: Int! + ) { + _meta { + block { + number + } + } + dataSets( + where: { + fwssServiceProvider: $serviceProvider + fwssPayer_not: $payer + isActive: true + } + first: $datasetLimit + orderBy: createdAt + orderDirection: desc + subgraphError: allow + ) { + setId + withIPFSIndexing + pdpPaymentEndEpoch + roots( + where: { removed: false } + first: $pieceLimit + orderBy: createdAt + orderDirection: desc + ) { + rootId + cid + rawSize + ipfsRootCID + } + } + } + `, } as const; diff --git a/apps/backend/src/pdp-subgraph/types.ts b/apps/backend/src/pdp-subgraph/types.ts index ad8dcdc4..ffb66e57 100644 --- a/apps/backend/src/pdp-subgraph/types.ts +++ b/apps/backend/src/pdp-subgraph/types.ts @@ -1,4 +1,5 @@ import Joi from "joi"; +import { CID } from "multiformats/cid"; import { Hex, isAddress } from "viem"; // ----------------------------------------- @@ -54,6 +55,53 @@ export type ProviderDataSetResponse = { }[]; }; +/** A piece eligible for anonymous retrieval. */ +export type FwssCandidatePiece = { + /** Decoded piece CID string (e.g. "bafk..."). */ + pieceCid: string; + /** On-chain piece ID (rootId) as a decimal string. */ + pieceId: string; + /** On-chain dataset ID (setId) as a decimal string. */ + dataSetId: string; + /** Raw piece size in bytes, as a decimal string. */ + rawSize: string; + /** True iff the parent dataset declared withIPFSIndexing metadata. */ + withIPFSIndexing: boolean; + /** IPFS root CID declared by the client when uploading, or null. */ + ipfsRootCid: string | null; +}; + +/** + * Validated raw shape of the FWSS candidate-pieces subgraph response. + * Consumers should prefer the parsed FwssCandidatePiece[] output. + */ +export type RawCandidatePiecesResponse = { + _meta: { block: { number: number } }; + dataSets: Array<{ + setId: string; + withIPFSIndexing: boolean; + pdpPaymentEndEpoch: string | null; + roots: Array<{ + rootId: string; + cid: string; + rawSize: string; + ipfsRootCID: string | null; + }>; + }>; +}; + +// ----------------------------------------- +// Helpers +// ----------------------------------------- + +/** + * Decodes a hex-encoded CID (0x...) into its string representation. + */ +export function decodePieceCid(hexData: string): string { + const bytes = Buffer.from(hexData.slice(2), "hex"); + return CID.decode(new Uint8Array(bytes)).toString(); +} + // ----------------------------------------- // Joi Custom Schema Converters // ----------------------------------------- @@ -117,6 +165,37 @@ const providerDataSetResponseSchema = Joi.object({ .unknown(true) .required(); +const candidateRootSchema = Joi.object({ + rootId: Joi.string().pattern(/^\d+$/).required(), + cid: Joi.string() + .pattern(/^0x[0-9a-fA-F]+$/) + .required(), + rawSize: Joi.string().pattern(/^\d+$/).required(), + ipfsRootCID: Joi.string().allow(null).optional(), +}).unknown(true); + +const candidateDataSetSchema = Joi.object({ + setId: Joi.string().pattern(/^\d+$/).required(), + withIPFSIndexing: Joi.boolean().required(), + pdpPaymentEndEpoch: Joi.string().pattern(/^\d+$/).allow(null).optional(), + roots: Joi.array().items(candidateRootSchema).required(), +}).unknown(true); + +const candidatePiecesResponseSchema = Joi.object({ + _meta: Joi.object({ + block: Joi.object({ + number: Joi.number().integer().positive().required(), + }) + .unknown(true) + .required(), + }) + .unknown(true) + .required(), + dataSets: Joi.array().items(candidateDataSetSchema).required(), +}) + .unknown(true) + .required(); + // ----------------------------------------- // Validator Functions // ----------------------------------------- @@ -149,3 +228,16 @@ export function validateProviderDataSetResponse(value: unknown): ProviderDataSet } return validated as ProviderDataSetResponse; } + +/** + * Validates the raw FWSS candidate-pieces response from the subgraph. + * + * @throws Error if validation fails + */ +export function validateCandidatePiecesResponse(value: unknown): RawCandidatePiecesResponse { + const { error, value: validated } = candidatePiecesResponseSchema.validate(value, { abortEarly: false }); + if (error) { + throw new Error(`Invalid candidate pieces response format: ${error.message}`); + } + return validated as RawCandidatePiecesResponse; +} diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts new file mode 100644 index 00000000..306a6a0c --- /dev/null +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts @@ -0,0 +1,136 @@ +import type { ConfigService } from "@nestjs/config"; +import type { Repository } from "typeorm"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { IConfig } from "../config/app.config.js"; +import type { Retrieval } from "../database/entities/retrieval.entity.js"; +import type { PDPSubgraphService } from "../pdp-subgraph/pdp-subgraph.service.js"; +import type { FwssCandidatePiece } from "../pdp-subgraph/types.js"; +import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; + +const SP_ADDRESS = "0xAaAaAAaAaaaAaAAAAaaaaAAaaAaaaAAaaaaa1111"; +const DEALBOT_PAYER = "0xBbBBBbBBbbbBbBBBBBbbbbbBBbbBbbbBBbbbb2222"; + +const makePiece = (overrides: Partial = {}): FwssCandidatePiece => ({ + pieceCid: `baga6ea4seaqpiece${Math.random().toString(36).slice(2, 10)}`, + pieceId: "1", + dataSetId: "42", + rawSize: "1048576", + withIPFSIndexing: true, + ipfsRootCid: "bafyroot", + ...overrides, +}); + +const makeRetrievalRepository = (recentPieceCids: string[]): Repository => { + const queryBuilder = { + select: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + andWhere: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), + getRawMany: vi.fn().mockResolvedValue(recentPieceCids.map((c) => ({ anonPieceCid: c }))), + }; + return { + createQueryBuilder: vi.fn().mockReturnValue(queryBuilder), + } as unknown as Repository; +}; + +const makeConfigService = (): ConfigService => + ({ + get: vi.fn((key: string) => { + if (key === "blockchain") { + return { walletAddress: DEALBOT_PAYER }; + } + return undefined; + }), + }) as unknown as ConfigService; + +describe("AnonPieceSelectorService", () => { + let subgraphService: PDPSubgraphService; + let listFwssCandidatePieces: ReturnType; + + beforeEach(() => { + listFwssCandidatePieces = vi.fn(); + subgraphService = { listFwssCandidatePieces } as unknown as PDPSubgraphService; + }); + + it("returns null when the subgraph yields no candidates", async () => { + listFwssCandidatePieces.mockResolvedValue([]); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(result).toBeNull(); + expect(listFwssCandidatePieces).toHaveBeenCalledWith(SP_ADDRESS, DEALBOT_PAYER); + }); + + it("filters out pieces tested in the recent retrieval window", async () => { + const freshCid = "baga6ea4seaqfresh"; + const staleCid = "baga6ea4seaqstale"; + listFwssCandidatePieces.mockResolvedValue([ + makePiece({ pieceCid: staleCid, pieceId: "1" }), + makePiece({ pieceCid: freshCid, pieceId: "2" }), + ]); + const service = new AnonPieceSelectorService( + subgraphService, + makeConfigService(), + makeRetrievalRepository([staleCid]), + ); + + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(result).not.toBeNull(); + expect(result?.pieceCid).toBe(freshCid); + }); + + it("falls back to the full candidate pool when every piece has been tested recently", async () => { + const cid = "baga6ea4seaqonly"; + listFwssCandidatePieces.mockResolvedValue([makePiece({ pieceCid: cid })]); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([cid])); + + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(result?.pieceCid).toBe(cid); + }); + + it("prefers IPFS-indexed pieces with an ipfsRootCid when selecting", async () => { + const pieces = [ + makePiece({ pieceCid: "baga-plain-1", withIPFSIndexing: false, ipfsRootCid: null }), + makePiece({ pieceCid: "baga-indexed-1", withIPFSIndexing: true, ipfsRootCid: "bafy1" }), + makePiece({ pieceCid: "baga-plain-2", withIPFSIndexing: false, ipfsRootCid: null }), + makePiece({ pieceCid: "baga-indexed-2", withIPFSIndexing: true, ipfsRootCid: "bafy2" }), + ]; + listFwssCandidatePieces.mockResolvedValue(pieces); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + + const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0); + + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(result?.pieceCid).toBe("baga-indexed-1"); + randomSpy.mockRestore(); + }); + + it("falls back to all pieces when none are IPFS-indexed", async () => { + const pieces = [ + makePiece({ pieceCid: "baga-plain-1", withIPFSIndexing: false, ipfsRootCid: null }), + makePiece({ pieceCid: "baga-plain-2", withIPFSIndexing: true, ipfsRootCid: null }), + ]; + listFwssCandidatePieces.mockResolvedValue(pieces); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + + const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0); + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(["baga-plain-1", "baga-plain-2"]).toContain(result?.pieceCid); + randomSpy.mockRestore(); + }); + + it("returns lowercase SP address on the selected piece", async () => { + listFwssCandidatePieces.mockResolvedValue([makePiece()]); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(result?.serviceProvider).toBe(SP_ADDRESS.toLowerCase()); + }); +}); diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts new file mode 100644 index 00000000..fc083014 --- /dev/null +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts @@ -0,0 +1,100 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { InjectRepository } from "@nestjs/typeorm"; +import type { Repository } from "typeorm"; +import type { IConfig } from "../config/app.config.js"; +import { Retrieval } from "../database/entities/retrieval.entity.js"; +import { PDPSubgraphService } from "../pdp-subgraph/pdp-subgraph.service.js"; +import type { FwssCandidatePiece } from "../pdp-subgraph/types.js"; +import type { AnonPiece } from "./types.js"; + +/** + * Number of most-recently-tested anonymous pieces to exclude from selection + * to avoid immediately retesting the same piece. Piece CIDs are globally + * unique and each one lives on a single SP's dataset, so scoping by CID + * is equivalent to scoping by (SP, CID) for this workload. + */ +const RECENT_DEDUP_WINDOW = 500; + +@Injectable() +export class AnonPieceSelectorService { + private readonly logger = new Logger(AnonPieceSelectorService.name); + + constructor( + private readonly pdpSubgraphService: PDPSubgraphService, + private readonly configService: ConfigService, + @InjectRepository(Retrieval) + private readonly retrievalRepository: Repository, + ) {} + + /** + * Select an anonymous piece to test against the given SP. + * + * Queries the FWSS subgraph for candidate pieces, filters out pieces + * tested in the last RECENT_DEDUP_WINDOW anonymous retrievals, and + * picks one uniformly at random — preferring pieces with a declared + * ipfsRootCID so CAR/IPNI validation has something meaningful to check. + */ + async selectPieceForProvider(spAddress: string): Promise { + const dealbotPayer = this.configService.get("blockchain", { infer: true }).walletAddress; + const candidates = await this.pdpSubgraphService.listFwssCandidatePieces(spAddress, dealbotPayer); + + if (candidates.length === 0) { + this.logger.warn({ + event: "anon_no_candidates", + message: "FWSS subgraph returned no candidate pieces for SP", + spAddress, + }); + return null; + } + + const recentlyTested = await this.loadRecentlyTestedPieceCids(); + const fresh = candidates.filter((c) => !recentlyTested.has(c.pieceCid)); + const pool = fresh.length > 0 ? fresh : candidates; + + const picked = this.pickPreferringIpfsIndexed(pool); + + this.logger.log({ + event: "anon_piece_selected", + message: "Selected anonymous piece for retrieval test", + spAddress, + pieceCid: picked.pieceCid, + dataSetId: picked.dataSetId, + withIPFSIndexing: picked.withIPFSIndexing, + candidateCount: candidates.length, + freshCount: fresh.length, + }); + + return { + pieceCid: picked.pieceCid, + dataSetId: picked.dataSetId, + pieceId: picked.pieceId, + serviceProvider: spAddress.toLowerCase(), + withIPFSIndexing: picked.withIPFSIndexing, + ipfsRootCid: picked.ipfsRootCid, + }; + } + + /** + * Return the set of piece CIDs tested in the last RECENT_DEDUP_WINDOW + * anonymous retrievals across all SPs. + */ + private async loadRecentlyTestedPieceCids(): Promise> { + const rows = await this.retrievalRepository + .createQueryBuilder("r") + .select("r.anon_piece_cid", "anonPieceCid") + .where("r.is_anonymous = true") + .andWhere("r.anon_piece_cid IS NOT NULL") + .orderBy("r.created_at", "DESC") + .limit(RECENT_DEDUP_WINDOW) + .getRawMany<{ anonPieceCid: string }>(); + + return new Set(rows.map((row) => row.anonPieceCid)); + } + + private pickPreferringIpfsIndexed(pool: FwssCandidatePiece[]): FwssCandidatePiece { + const ipfsIndexed = pool.filter((p) => p.withIPFSIndexing && p.ipfsRootCid); + const effective = ipfsIndexed.length > 0 ? ipfsIndexed : pool; + return effective[Math.floor(Math.random() * effective.length)]; + } +} diff --git a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts new file mode 100644 index 00000000..b87962cd --- /dev/null +++ b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts @@ -0,0 +1,180 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import type { Repository } from "typeorm"; +import { type ProviderJobContext, toStructuredError } from "../common/logging.js"; +import { Retrieval } from "../database/entities/retrieval.entity.js"; +import { StorageProvider } from "../database/entities/storage-provider.entity.js"; +import { RetrievalStatus, ServiceType } from "../database/types.js"; +import { buildCheckMetricLabels } from "../metrics-prometheus/check-metric-labels.js"; +import { AnonRetrievalCheckMetrics } from "../metrics-prometheus/check-metrics.service.js"; +import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; +import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; +import { CarValidationService } from "./car-validation.service.js"; +import { PieceRetrievalService } from "./piece-retrieval.service.js"; +import type { CarValidationResult } from "./types.js"; + +@Injectable() +export class AnonRetrievalService { + private readonly logger = new Logger(AnonRetrievalService.name); + + constructor( + private readonly anonPieceSelectorService: AnonPieceSelectorService, + private readonly pieceRetrievalService: PieceRetrievalService, + private readonly carValidationService: CarValidationService, + private readonly walletSdkService: WalletSdkService, + private readonly metrics: AnonRetrievalCheckMetrics, + @InjectRepository(Retrieval) + private readonly retrievalRepository: Repository, + @InjectRepository(StorageProvider) + private readonly spRepository: Repository, + ) {} + + async performForProvider( + spAddress: string, + signal?: AbortSignal, + logContext?: ProviderJobContext, + ): Promise { + // Build metric labels + const provider = await this.spRepository.findOne({ where: { address: spAddress } }); + const labels = buildCheckMetricLabels({ + checkType: "anon_retrieval", + providerId: provider?.providerId, + providerName: provider?.name, + providerIsApproved: provider?.isApproved, + }); + + // 1. Select an anonymous piece + const piece = await this.anonPieceSelectorService.selectPieceForProvider(spAddress); + if (!piece) { + this.logger.warn({ + ...logContext, + event: "anon_retrieval_no_piece", + message: "No anonymous piece found for SP", + spAddress, + }); + this.metrics.recordStatus(labels, "failure.no_piece"); + return null; + } + + this.logger.log({ + ...logContext, + event: "anon_retrieval_started", + message: "Starting anonymous retrieval test", + pieceCid: piece.pieceCid, + dataSetId: piece.dataSetId, + pieceId: piece.pieceId, + withIPFSIndexing: piece.withIPFSIndexing, + spAddress, + }); + + const checkStart = Date.now(); + const startedAt = new Date(); + + // 2. Fetch the piece + signal?.throwIfAborted(); + const pieceResult = await this.pieceRetrievalService.fetchPiece(spAddress, piece.pieceCid, signal); + + // Emit piece retrieval metrics + this.metrics.observeFirstByteMs(labels, pieceResult.ttfbMs); + this.metrics.observeLastByteMs(labels, pieceResult.latencyMs); + this.metrics.observeThroughput(labels, pieceResult.throughputBps); + this.metrics.recordHttpResponseCode(labels, pieceResult.statusCode); + + // 3. CAR validation (only if piece was successfully retrieved and has IPFS indexing) + let carResult: CarValidationResult | null = null; + if (pieceResult.success && piece.withIPFSIndexing && piece.ipfsRootCid && pieceResult.pieceBytes && provider) { + signal?.throwIfAborted(); + try { + carResult = await this.carValidationService.validateCarPiece( + pieceResult.pieceBytes, + provider, + piece.ipfsRootCid, + signal, + ); + } catch (error) { + this.logger.warn({ + ...logContext, + event: "anon_retrieval_car_validation_failed", + message: "CAR validation threw an error", + pieceCid: piece.pieceCid, + spAddress, + error: toStructuredError(error), + }); + } + } + + // Emit CAR validation metrics + if (carResult) { + this.metrics.recordCarParseStatus(labels, carResult.carParseable); + this.metrics.recordIpniStatus( + labels, + carResult.ipniValid === null ? "skipped" : carResult.ipniValid ? "valid" : "invalid", + ); + this.metrics.recordBlockFetchStatus( + labels, + carResult.blockFetchValid === null ? "skipped" : carResult.blockFetchValid ? "valid" : "invalid", + ); + } else if (!pieceResult.success) { + // Piece retrieval failed — IPNI and block fetch were skipped + this.metrics.recordIpniStatus(labels, "skipped"); + this.metrics.recordBlockFetchStatus(labels, "skipped"); + } + + // Overall check duration and status + this.metrics.observeCheckDuration(labels, Date.now() - checkStart); + this.metrics.recordStatus(labels, pieceResult.success ? "success" : "failure.http"); + + // 4. Build the SP base URL for the retrieval endpoint + const providerInfo = this.walletSdkService.getProviderInfo(spAddress); + const spBaseUrl = providerInfo?.pdp.serviceURL.replace(/\/$/, "") ?? spAddress; + + // 5. Save retrieval record + const retrieval = this.retrievalRepository.create({ + isAnonymous: true, + anonPieceCid: piece.pieceCid, + anonDataSetId: piece.dataSetId, + anonPieceId: piece.pieceId, + serviceType: ServiceType.DIRECT_SP, + retrievalEndpoint: `${spBaseUrl}/piece/${piece.pieceCid}`, + status: pieceResult.success ? RetrievalStatus.SUCCESS : RetrievalStatus.FAILED, + startedAt, + completedAt: new Date(), + latencyMs: Math.round(pieceResult.latencyMs), + ttfbMs: Math.round(pieceResult.ttfbMs), + throughputBps: Math.round(pieceResult.throughputBps), + bytesRetrieved: pieceResult.bytesReceived, + responseCode: pieceResult.statusCode, + errorMessage: pieceResult.errorMessage, + commPValid: pieceResult.success ? pieceResult.commPValid : undefined, + carValid: carResult ? carResult.ipniValid !== false && carResult.blockFetchValid !== false : undefined, + }); + + try { + await this.retrievalRepository.save(retrieval); + } catch (error) { + this.logger.warn({ + ...logContext, + event: "anon_retrieval_save_failed", + message: "Failed to save anonymous retrieval record", + pieceCid: piece.pieceCid, + spAddress, + error: toStructuredError(error), + }); + } + + this.logger.log({ + ...logContext, + event: "anon_retrieval_completed", + message: "Anonymous retrieval test completed", + pieceCid: piece.pieceCid, + spAddress, + success: pieceResult.success, + latencyMs: pieceResult.latencyMs, + carParseable: carResult?.carParseable, + ipniValid: carResult?.ipniValid, + blockFetchValid: carResult?.blockFetchValid, + }); + + return retrieval; + } +} diff --git a/apps/backend/src/retrieval-anon/car-validation.service.ts b/apps/backend/src/retrieval-anon/car-validation.service.ts new file mode 100644 index 00000000..8019b8df --- /dev/null +++ b/apps/backend/src/retrieval-anon/car-validation.service.ts @@ -0,0 +1,223 @@ +import { CarReader } from "@ipld/car"; +import * as dagPB from "@ipld/dag-pb"; +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { create as createBlock } from "multiformats/block"; +import { CID } from "multiformats/cid"; +import * as raw from "multiformats/codecs/raw"; +import { sha256 } from "multiformats/hashes/sha2"; +import { toStructuredError } from "../common/logging.js"; +import type { IConfig } from "../config/app.config.js"; +import type { StorageProvider } from "../database/entities/storage-provider.entity.js"; +import { HttpClientService } from "../http-client/http-client.service.js"; +import { IpniVerificationService } from "../ipni/ipni-verification.service.js"; +import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; +import type { CarValidationResult } from "./types.js"; + +// UnixFS DAGs use only dag-pb (interior nodes) and raw (leaf data) codecs +const unixfsCodecs: Record unknown }> = { + [dagPB.code]: dagPB, + [raw.code]: raw, +}; + +@Injectable() +export class CarValidationService { + private readonly logger = new Logger(CarValidationService.name); + + constructor( + private readonly configService: ConfigService, + private readonly httpClientService: HttpClientService, + private readonly walletSdkService: WalletSdkService, + private readonly ipniVerificationService: IpniVerificationService, + ) {} + + /** + * Validate an anonymous piece retrieved as a CAR: + * 1. parse the CAR, + * 2. sample random blocks, + * 3. confirm the SP is advertised for the root + sampled CIDs via IPNI, + * 4. fetch each sampled block from the SP and hash-verify it. + * + * CAR parse failure is attributed to the client (bad upload), not the SP. + */ + async validateCarPiece( + pieceBytes: Buffer, + provider: StorageProvider, + ipfsRootCid: string, + signal?: AbortSignal, + ): Promise { + const blocks = await this.parseCar(pieceBytes, provider.address, ipfsRootCid); + if (blocks === null) { + return { carParseable: false, blockCount: 0, sampledCidCount: 0, ipniValid: null, blockFetchValid: null }; + } + if (blocks.length === 0) { + return { + carParseable: true, + blockCount: 0, + sampledCidCount: 0, + ipniValid: null, + blockFetchValid: null, + errorMessage: "CAR contained no blocks", + }; + } + + const sampleCount = this.configService.get("retrieval", { infer: true }).anonBlockSampleCount; + const shuffled = [...blocks].sort(() => Math.random() - 0.5); + const sampledBlocks = shuffled.slice(0, sampleCount); + + const ipniValid = await this.checkIpni(provider, ipfsRootCid, sampledBlocks, signal); + const blockFetchResult = await this.checkBlockFetch(sampledBlocks, provider.address, signal); + + return { + carParseable: true, + blockCount: blocks.length, + sampledCidCount: sampledBlocks.length, + ipniValid, + blockFetchValid: blockFetchResult.valid, + errorMessage: blockFetchResult.errorMessage, + }; + } + + private async parseCar( + pieceBytes: Buffer, + spAddress: string, + ipfsRootCid: string, + ): Promise<{ cid: CID; bytes: Uint8Array }[] | null> { + try { + const reader = await CarReader.fromBytes(new Uint8Array(pieceBytes)); + const blocks: { cid: CID; bytes: Uint8Array }[] = []; + for await (const block of reader.blocks()) { + blocks.push({ cid: block.cid, bytes: block.bytes }); + } + return blocks; + } catch (error) { + this.logger.debug({ + event: "car_parse_failed", + message: "Failed to parse piece bytes as CAR - client fault, not SP", + spAddress, + ipfsRootCid, + error: toStructuredError(error), + }); + return null; + } + } + + /** + * Verify via IPNI that the SP is advertised for the root CID and each sampled child CID. + * Delegates to the shared IpniVerificationService which uses filecoin-pin's provider-scoped check. + */ + private async checkIpni( + provider: StorageProvider, + ipfsRootCid: string, + sampledBlocks: ReadonlyArray<{ cid: CID }>, + signal?: AbortSignal, + ): Promise { + const timeouts = this.configService.get("timeouts", { infer: true }); + let rootCid: CID; + try { + rootCid = CID.parse(ipfsRootCid); + } catch (error) { + this.logger.warn({ + event: "ipni_root_cid_invalid", + message: "Failed to parse ipfsRootCID", + ipfsRootCid, + providerAddress: provider.address, + error: toStructuredError(error), + }); + return false; + } + + const result = await this.ipniVerificationService.verify({ + rootCid, + blockCids: sampledBlocks.map((b) => b.cid), + storageProvider: provider, + timeoutMs: timeouts.ipniVerificationTimeoutMs, + pollIntervalMs: timeouts.ipniVerificationPollingMs, + signal, + }); + + return result.rootCIDVerified; + } + + /** + * Fetch each sampled block from the SP endpoint and hash-verify the response + * against the declared CID. Mirrors IpfsBlockRetrievalStrategy's per-block + * verification for the sampled subset (no DAG traversal). + */ + private async checkBlockFetch( + sampledBlocks: ReadonlyArray<{ cid: CID; bytes: Uint8Array }>, + spAddress: string, + signal?: AbortSignal, + ): Promise<{ valid: boolean | null; errorMessage?: string }> { + const providerInfo = this.walletSdkService.getProviderInfo(spAddress); + if (!providerInfo) { + return { valid: null, errorMessage: `Provider info not found for ${spAddress}` }; + } + + const spBaseUrl = providerInfo.pdp.serviceURL.replace(/\/$/, ""); + let allValid = true; + + for (const block of sampledBlocks) { + signal?.throwIfAborted(); + const cidStr = block.cid.toString(); + const blockUrl = `${spBaseUrl}/ipfs/${cidStr}?format=raw`; + + try { + const resp = await this.httpClientService.requestWithMetrics(blockUrl, { + headers: { Accept: "application/vnd.ipld.raw" }, + httpVersion: "2", + signal, + }); + + if (resp.metrics.statusCode < 200 || resp.metrics.statusCode >= 300) { + allValid = false; + this.logger.warn({ + event: "block_fetch_non_2xx", + message: "Block fetch returned non-2xx status", + cid: cidStr, + spAddress, + statusCode: resp.metrics.statusCode, + }); + continue; + } + + if (block.cid.multihash.code !== sha256.code) { + this.logger.warn({ + event: "block_unsupported_hash", + message: `Unsupported hash algorithm 0x${block.cid.multihash.code.toString(16)}`, + cid: cidStr, + spAddress, + }); + allValid = false; + continue; + } + + const codec = unixfsCodecs[block.cid.code]; + if (!codec) { + this.logger.warn({ + event: "block_unsupported_codec", + message: `Unsupported codec 0x${block.cid.code.toString(16)}`, + cid: cidStr, + spAddress, + }); + allValid = false; + continue; + } + + // Hash-verifies and decodes; throws on mismatch + await createBlock({ bytes: resp.data, cid: block.cid, hasher: sha256, codec }); + } catch (error) { + allValid = false; + this.logger.warn({ + event: "block_fetch_failed", + message: "Block fetch or hash verification failed", + cid: cidStr, + spAddress, + error: toStructuredError(error), + }); + } + } + + return { valid: allValid }; + } +} diff --git a/apps/backend/src/retrieval-anon/piece-retrieval.service.ts b/apps/backend/src/retrieval-anon/piece-retrieval.service.ts new file mode 100644 index 00000000..851f68ec --- /dev/null +++ b/apps/backend/src/retrieval-anon/piece-retrieval.service.ts @@ -0,0 +1,165 @@ +import { asPieceCID, calculate as calculatePieceCid } from "@filoz/synapse-core/piece"; +import { Injectable, Logger } from "@nestjs/common"; +import { toStructuredError } from "../common/logging.js"; +import { HttpClientService } from "../http-client/http-client.service.js"; +import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; +import type { PieceRetrievalResult } from "./types.js"; + +@Injectable() +export class PieceRetrievalService { + private readonly logger = new Logger(PieceRetrievalService.name); + + constructor( + private readonly walletSdkService: WalletSdkService, + private readonly httpClientService: HttpClientService, + ) {} + + async fetchPiece(spAddress: string, pieceCid: string, signal?: AbortSignal): Promise { + const providerInfo = this.walletSdkService.getProviderInfo(spAddress); + + if (!providerInfo) { + this.logger.warn({ + event: "provider_info_not_found", + message: "Cannot fetch piece: provider info not found", + spAddress, + pieceCid, + }); + + return { + success: false, + pieceCid, + bytesReceived: 0, + pieceBytes: null, + latencyMs: 0, + ttfbMs: 0, + throughputBps: 0, + statusCode: 0, + commPValid: false, + errorMessage: `Provider info not found for ${spAddress}`, + }; + } + + const baseUrl = providerInfo.pdp.serviceURL.replace(/\/$/, ""); + const url = `${baseUrl}/piece/${pieceCid}`; + + try { + const result = await this.httpClientService.requestWithMetrics(url, { + httpVersion: "2", + signal, + }); + + const { metrics } = result; + const isSuccess = metrics.statusCode >= 200 && metrics.statusCode < 300; + + if (!isSuccess) { + this.logger.warn({ + event: "piece_fetch_non_2xx", + message: "Piece fetch returned non-2xx status", + url, + statusCode: metrics.statusCode, + pieceCid, + spAddress, + }); + + return { + success: false, + pieceCid, + bytesReceived: metrics.responseSize, + pieceBytes: null, + latencyMs: metrics.totalTime, + ttfbMs: metrics.ttfb, + throughputBps: metrics.totalTime > 0 ? metrics.responseSize / (metrics.totalTime / 1000) : 0, + statusCode: metrics.statusCode, + commPValid: false, + errorMessage: `HTTP ${metrics.statusCode}`, + }; + } + + const pieceBytes = Buffer.isBuffer(result.data) ? result.data : Buffer.from(result.data); + const commPValid = await this.validateCommP(pieceBytes, pieceCid); + const throughputBps = metrics.totalTime > 0 ? metrics.responseSize / (metrics.totalTime / 1000) : 0; + + this.logger.debug({ + event: "piece_fetch_success", + message: "Piece fetched successfully", + pieceCid, + spAddress, + bytesReceived: metrics.responseSize, + latencyMs: metrics.totalTime, + ttfbMs: metrics.ttfb, + }); + + return { + success: true, + pieceCid, + bytesReceived: metrics.responseSize, + pieceBytes, + latencyMs: metrics.totalTime, + ttfbMs: metrics.ttfb, + throughputBps, + statusCode: metrics.statusCode, + commPValid, + }; + } catch (error) { + this.logger.warn({ + event: "piece_fetch_failed", + message: "Piece fetch threw an error", + url, + pieceCid, + spAddress, + error: toStructuredError(error), + }); + + return { + success: false, + pieceCid, + bytesReceived: 0, + pieceBytes: null, + latencyMs: 0, + ttfbMs: 0, + throughputBps: 0, + statusCode: 0, + commPValid: false, + errorMessage: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Compute the piece CID (sha2-256-trunc254-padded) of the retrieved bytes and compare + * against the expected CID. Returns false on parse failure, computation failure, or mismatch. + */ + private async validateCommP(bytes: Buffer, pieceCid: string): Promise { + const expected = asPieceCID(pieceCid); + if (!expected) { + this.logger.warn({ + event: "commp_invalid_piece_cid", + message: "Cannot parse expected piece CID for CommP validation", + pieceCid, + }); + return false; + } + + try { + const computed = calculatePieceCid(bytes); + const matches = computed.toString() === expected.toString(); + if (!matches) { + this.logger.warn({ + event: "commp_mismatch", + message: "Piece CID mismatch: SP-returned bytes hash to a different CID", + expected: expected.toString(), + computed: computed.toString(), + }); + } + return matches; + } catch (error) { + this.logger.warn({ + event: "commp_validation_error", + message: "CommP computation threw an error", + pieceCid, + error: toStructuredError(error), + }); + return false; + } + } +} diff --git a/apps/backend/src/retrieval-anon/retrieval-anon.module.ts b/apps/backend/src/retrieval-anon/retrieval-anon.module.ts new file mode 100644 index 00000000..ba799199 --- /dev/null +++ b/apps/backend/src/retrieval-anon/retrieval-anon.module.ts @@ -0,0 +1,27 @@ +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { Retrieval } from "../database/entities/retrieval.entity.js"; +import { StorageProvider } from "../database/entities/storage-provider.entity.js"; +import { HttpClientModule } from "../http-client/http-client.module.js"; +import { IpniModule } from "../ipni/ipni.module.js"; +import { PdpSubgraphModule } from "../pdp-subgraph/pdp-subgraph.module.js"; +import { WalletSdkModule } from "../wallet-sdk/wallet-sdk.module.js"; +import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; +import { AnonRetrievalService } from "./anon-retrieval.service.js"; +import { CarValidationService } from "./car-validation.service.js"; +import { PieceRetrievalService } from "./piece-retrieval.service.js"; + +@Module({ + imports: [ + ConfigModule, + TypeOrmModule.forFeature([Retrieval, StorageProvider]), + PdpSubgraphModule, + WalletSdkModule, + HttpClientModule, + IpniModule, + ], + providers: [AnonPieceSelectorService, PieceRetrievalService, CarValidationService, AnonRetrievalService], + exports: [AnonRetrievalService], +}) +export class RetrievalAnonModule {} diff --git a/apps/backend/src/retrieval-anon/types.ts b/apps/backend/src/retrieval-anon/types.ts new file mode 100644 index 00000000..03c61712 --- /dev/null +++ b/apps/backend/src/retrieval-anon/types.ts @@ -0,0 +1,33 @@ +/** The result of anonymous piece selection. */ +export type AnonPiece = { + pieceCid: string; + dataSetId: string; + pieceId: string; + serviceProvider: string; + withIPFSIndexing: boolean; + ipfsRootCid: string | null; +}; + +/** Result of piece retrieval. */ +export type PieceRetrievalResult = { + success: boolean; + pieceCid: string; + bytesReceived: number; + pieceBytes: Buffer | null; + latencyMs: number; + ttfbMs: number; + throughputBps: number; + statusCode: number; + commPValid: boolean; + errorMessage?: string; +}; + +/** Result of CAR validation. */ +export type CarValidationResult = { + carParseable: boolean; + blockCount: number; + sampledCidCount: number; + ipniValid: boolean | null; + blockFetchValid: boolean | null; + errorMessage?: string; +}; From dd47e6b30425ac9d4adb98d123fcb0cb7114387b Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 08:40:27 +0200 Subject: [PATCH 02/19] feat(subgraph): add @dealbot/subgraph package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Imports the goldsky subgraph mappings from FilOzone/pdp-explorer#100 as an in-tree package. This is the subgraph dealbot will own and deploy for itself (motivated by dealbot#427 anonymous retrieval check). Integrated with pnpm workspace, parameterized over networks.json for mainnet (filecoin) and calibration (filecoin-testnet), and pinned assemblyscript@0.19.23 so matchstick-as@0.6.0 picks up its binary. Biome and root test/build scripts intentionally skip this package — it is AssemblyScript compiled to WASM via graph-cli, and its lifecycle is "rebuild and redeploy to Goldsky", not per-PR. Schema, handlers, and tests are currently the unmodified upstream pdp-explorer content; subsequent commits will trim them to the three queries dealbot actually uses. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/subgraph/.gitignore | 17 + apps/subgraph/README.md | 76 + .../abis/FilecoinWarmStorageService.json | 2389 +++++++++++++++++ apps/subgraph/abis/PDPService.json | 649 +++++ apps/subgraph/abis/PDPVerifier.json | 1266 +++++++++ apps/subgraph/networks.json | 22 + apps/subgraph/package.json | 21 + apps/subgraph/schema.graphql | 305 +++ apps/subgraph/src/fwss.ts | 182 ++ apps/subgraph/src/helper.ts | 238 ++ apps/subgraph/src/pdp-service.ts | 371 +++ apps/subgraph/src/pdp-verifier.ts | 1603 +++++++++++ apps/subgraph/src/sumTree.ts | 200 ++ apps/subgraph/src/types.ts | 6 + apps/subgraph/subgraph.yaml | 84 + apps/subgraph/tests/dataset-status.test.ts | 548 ++++ apps/subgraph/tests/fault-calculation.test.ts | 962 +++++++ apps/subgraph/tests/fwss-utils.ts | 246 ++ apps/subgraph/tests/fwss.test.ts | 455 ++++ apps/subgraph/tests/pdp-verifier-utils.ts | 273 ++ apps/subgraph/tests/pdp-verifier.test.ts | 150 ++ apps/subgraph/tsconfig.json | 4 + apps/subgraph/utils/cid.ts | 118 + apps/subgraph/utils/index.ts | 15 + pnpm-lock.yaml | 1983 +++++++++++++- 25 files changed, 12080 insertions(+), 103 deletions(-) create mode 100644 apps/subgraph/.gitignore create mode 100644 apps/subgraph/README.md create mode 100644 apps/subgraph/abis/FilecoinWarmStorageService.json create mode 100644 apps/subgraph/abis/PDPService.json create mode 100644 apps/subgraph/abis/PDPVerifier.json create mode 100644 apps/subgraph/networks.json create mode 100644 apps/subgraph/package.json create mode 100644 apps/subgraph/schema.graphql create mode 100644 apps/subgraph/src/fwss.ts create mode 100644 apps/subgraph/src/helper.ts create mode 100644 apps/subgraph/src/pdp-service.ts create mode 100644 apps/subgraph/src/pdp-verifier.ts create mode 100644 apps/subgraph/src/sumTree.ts create mode 100644 apps/subgraph/src/types.ts create mode 100644 apps/subgraph/subgraph.yaml create mode 100644 apps/subgraph/tests/dataset-status.test.ts create mode 100644 apps/subgraph/tests/fault-calculation.test.ts create mode 100644 apps/subgraph/tests/fwss-utils.ts create mode 100644 apps/subgraph/tests/fwss.test.ts create mode 100644 apps/subgraph/tests/pdp-verifier-utils.ts create mode 100644 apps/subgraph/tests/pdp-verifier.test.ts create mode 100644 apps/subgraph/tsconfig.json create mode 100644 apps/subgraph/utils/cid.ts create mode 100644 apps/subgraph/utils/index.ts diff --git a/apps/subgraph/.gitignore b/apps/subgraph/.gitignore new file mode 100644 index 00000000..931a409a --- /dev/null +++ b/apps/subgraph/.gitignore @@ -0,0 +1,17 @@ +# graph-cli outputs +build/ +generated/ + +# Node dependencies +node_modules/ + +# Goldsky deploy artifacts +.goldsky/ + +# Test outputs +coverage/ +tests/.bin/ +tests/.latest.json + +# Editor / OS +.DS_Store diff --git a/apps/subgraph/README.md b/apps/subgraph/README.md new file mode 100644 index 00000000..26332b00 --- /dev/null +++ b/apps/subgraph/README.md @@ -0,0 +1,76 @@ +# @dealbot/subgraph + +A dealbot-owned Graph Protocol subgraph indexing the Filecoin PDP contracts. Deployed to Goldsky and consumed exclusively by `apps/backend` via the `PDP_SUBGRAPH_ENDPOINT` env var. + +## What it indexes + +- **PDPVerifier** — dataset lifecycle, piece add/remove, proving periods. +- **FilecoinWarmStorageService (FWSS)** — payer/service-provider metadata, `withIPFSIndexing` flag, `ipfsRootCID` per piece, service/payment termination. + +## Why it exists + +The dealbot backend needs three queries (see `apps/backend/src/pdp-subgraph/queries.ts`): + +1. `GET_SUBGRAPH_META` — latest indexed block. +2. `GET_PROVIDERS_WITH_DATASETS` — overdue proving-period detection. +3. `GET_FWSS_CANDIDATE_PIECES` — anonymous-retrieval piece selection (motivated by [FilOzone/dealbot#427](https://github.com/FilOzone/dealbot/issues/427)). + +The code originated as a fork of [FilOzone/pdp-explorer#100](https://github.com/FilOzone/pdp-explorer/pull/100). Forking lets us trim the schema and handlers to exactly what dealbot queries, and deploy on our own cadence. + +## Why this package is an outlier + +Subgraph mappings compile to WASM via AssemblyScript. Despite the `.ts` extension, AssemblyScript is **not** TypeScript: + +- No Biome/Prettier — the parser trips on AssemblyScript primitives (`u8`, `u32`, `i32`). +- Tests use `matchstick-as`, not Vitest. +- `tsconfig.json` extends `@graphprotocol/graph-ts`'s base config, not the monorepo's. +- Build is `graph codegen && graph build`, not `tsc` or `vite build`. + +The package is intentionally isolated from the root `pnpm test` / `pnpm build` scripts — its lifecycle is "rebuild and redeploy to Goldsky when mappings change", not "build on every PR". + +## Contract addresses + +| Network | Contract | Address | Start block | +|---|---|---|---| +| mainnet (`filecoin`) | PDPVerifier | `0xBADd0B92C1c71d02E7d520f64c0876538fa2557F` | 5441432 | +| mainnet (`filecoin`) | FilecoinWarmStorageService | `0x8408502033C418E1bbC97cE9ac48E5528F371A9f` | 5459617 | +| calibration (`filecoin-testnet`) | PDPVerifier | `0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C` | 3140755 | +| calibration (`filecoin-testnet`) | FilecoinWarmStorageService | `0x02925630df557F957f70E112bA06e50965417CA0` | 3141276 | + +Maintained in `networks.json`. Editing `subgraph.yaml` manually is usually wrong — run `pnpm build:mainnet` or `pnpm build:calibnet` which applies `networks.json` via `graph build --network `. + +Note: `graph build --network X` rewrites `subgraph.yaml` **in place** with the chosen network's values. The committed version is mainnet-default — after a `build:calibnet`, re-run `build:mainnet` before committing to avoid leaking calibnet values into the mainnet manifest. + +## Local commands + +```bash +# Typegen only (no WASM build) +pnpm --filter @dealbot/subgraph codegen + +# Full build for one network +pnpm --filter @dealbot/subgraph build:mainnet +pnpm --filter @dealbot/subgraph build:calibnet + +# Run matchstick tests +pnpm --filter @dealbot/subgraph test +``` + +## Deploy + +Requires `goldsky` CLI authenticated via `GOLDSKY_API_KEY`. + +```bash +export VERSION=0.1.0 +pnpm --filter @dealbot/subgraph build:calibnet +pnpm --filter @dealbot/subgraph deploy:calibnet + +pnpm --filter @dealbot/subgraph build:mainnet +pnpm --filter @dealbot/subgraph deploy:mainnet +``` + +Goldsky slots (slugs TBD): + +- `dealbot-subgraph/` — mainnet +- `dealbot-subgraph-calibnet/` — calibration + +After deploy, update `PDP_SUBGRAPH_ENDPOINT` in the backend env to the new `/gn` URL. diff --git a/apps/subgraph/abis/FilecoinWarmStorageService.json b/apps/subgraph/abis/FilecoinWarmStorageService.json new file mode 100644 index 00000000..cedddc45 --- /dev/null +++ b/apps/subgraph/abis/FilecoinWarmStorageService.json @@ -0,0 +1,2389 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_pdpVerifierAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_paymentsContractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_usdfc", + "type": "address", + "internalType": "contract IERC20Metadata" + }, + { + "name": "_filBeamBeneficiaryAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_serviceProviderRegistry", + "type": "address", + "internalType": "contract ServiceProviderRegistry" + }, + { + "name": "_sessionKeyRegistry", + "type": "address", + "internalType": "contract SessionKeyRegistry" + }, + { + "name": "_reinitializer_version", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addApprovedProvider", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "announcePlannedUpgrade", + "inputs": [ + { + "name": "plannedUpgrade", + "type": "tuple", + "internalType": "struct FilecoinWarmStorageService.PlannedUpgrade", + "components": [ + { + "name": "nextImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "afterEpoch", + "type": "uint96", + "internalType": "uint96" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "calculateRatePerEpoch", + "inputs": [ + { + "name": "totalBytes", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "storageRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "configureProvingPeriod", + "inputs": [ + { + "name": "_maxProvingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_challengeWindowSize", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dataSetCreated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "serviceProvider", + "type": "address", + "internalType": "address" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dataSetDeleted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { + "name": "fields", + "type": "bytes1", + "internalType": "bytes1" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "version", + "type": "string", + "internalType": "string" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "extensions", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "extsload", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "extsloadStruct", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "size", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "filBeamBeneficiaryAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getEffectiveRates", + "inputs": [], + "outputs": [ + { + "name": "serviceFee", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "spPayment", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getProvingPeriodForEpoch", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "epoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getServicePrice", + "inputs": [], + "outputs": [ + { + "name": "pricing", + "type": "tuple", + "internalType": "struct FilecoinWarmStorageService.ServicePricing", + "components": [ + { + "name": "pricePerTiBPerMonthNoCDN", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pricePerTiBCdnEgress", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pricePerTiBCacheMissEgress", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "contract IERC20" + }, + { + "name": "epochsPerMonth", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumPricePerMonth", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "_maxProvingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "_challengeWindowSize", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_filBeamControllerAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_name", + "type": "string", + "internalType": "string" + }, + { + "name": "_description", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "migrate", + "inputs": [ + { + "name": "_viewContract", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nextProvingPeriod", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "leafCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paymentsContractAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pdpVerifierAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "piecesAdded", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "firstAdded", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceData", + "type": "tuple[]", + "internalType": "struct Cids.Cid[]", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "piecesScheduledRemove", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "possessionProven", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "railTerminated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "terminator", + "type": "address", + "internalType": "address" + }, + { + "name": "endEpoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeApprovedProvider", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "serviceProviderRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ServiceProviderRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "sessionKeyRegistry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract SessionKeyRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setViewContract", + "inputs": [ + { + "name": "_viewContract", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "settleFilBeamPaymentRails", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cdnAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cacheMissAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "storageProviderChanged", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateCDNService", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateService", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "topUpCDNPaymentRails", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cdnAmountToAdd", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "cacheMissAmountToAdd", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFilBeamController", + "inputs": [ + { + "name": "newController", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updatePricing", + "inputs": [ + { + "name": "newStoragePrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "newMinimumRate", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateServiceCommission", + "inputs": [ + { + "name": "newCommissionBps", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "usdfcTokenAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20Metadata" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "validatePayment", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proposedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fromEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "toEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "result", + "type": "tuple", + "internalType": "struct IValidator.ValidationResult", + "components": [ + { + "name": "modifiedAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "settleUpto", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "note", + "type": "string", + "internalType": "string" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "viewContractAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "CDNPaymentRailsToppedUp", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "cdnAmountAdded", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalCdnLockup", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissAmountAdded", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalCacheMissLockup", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CDNPaymentTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CDNServiceTerminated", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ContractUpgraded", + "inputs": [ + { + "name": "version", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "implementation", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetCreated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "providerId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pdpRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "payer", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "serviceProvider", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "payee", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "metadataKeys", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + }, + { + "name": "metadataValues", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetServiceProviderChanged", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "oldServiceProvider", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newServiceProvider", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "FaultRecord", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "periodsFaulted", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FilBeamControllerChanged", + "inputs": [ + { + "name": "oldController", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "newController", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FilecoinServiceDeployed", + "inputs": [ + { + "name": "name", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PDPPaymentTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "pdpRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PieceAdded", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pieceId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pieceCid", + "type": "tuple", + "indexed": false, + "internalType": "struct Cids.Cid", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "keys", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + }, + { + "name": "values", + "type": "string[]", + "indexed": false, + "internalType": "string[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PricingUpdated", + "inputs": [ + { + "name": "storagePrice", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "minimumRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ProviderApproved", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ProviderUnapproved", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RailRateUpdated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "railId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ServiceTerminated", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pdpRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cacheMissRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "cdnRailId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "UpgradeAnnounced", + "inputs": [ + { + "name": "plannedUpgrade", + "type": "tuple", + "indexed": false, + "internalType": "struct FilecoinWarmStorageService.PlannedUpgrade", + "components": [ + { + "name": "nextImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "afterEpoch", + "type": "uint96", + "internalType": "uint96" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ViewContractSet", + "inputs": [ + { + "name": "viewContract", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AtLeastOnePriceMustBeNonZero", + "inputs": [] + }, + { + "type": "error", + "name": "CDNPaymentAlreadyTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "CacheMissPaymentAlreadyTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "CallerNotPayer", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expectedPayer", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerNotPayerOrPayee", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expectedPayer", + "type": "address", + "internalType": "address" + }, + { + "name": "expectedPayee", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "CallerNotPayments", + "inputs": [ + { + "name": "expected", + "type": "address", + "internalType": "address" + }, + { + "name": "actual", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ChallengeWindowTooEarly", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "windowStart", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nowBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ClientDataSetAlreadyRegistered", + "inputs": [ + { + "name": "clientDataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "CommissionExceedsMaximum", + "inputs": [ + { + "name": "commissionType", + "type": "uint8", + "internalType": "enum Errors.CommissionType" + }, + { + "name": "max", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetNotFoundForRail", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetNotRegistered", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetPaymentAlreadyTerminated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DataSetPaymentBeyondEndEpoch", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pdpEndEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "DivisionByZero", + "inputs": [] + }, + { + "type": "error", + "name": "DuplicateMetadataKey", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "key", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "ExtraDataRequired", + "inputs": [] + }, + { + "type": "error", + "name": "ExtraDataTooLarge", + "inputs": [ + { + "name": "actualSize", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowedSize", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "FilBeamServiceNotConfigured", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientLockupAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "lockupAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "lockupUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumLockupRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientLockupFunds", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "minimumRequired", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientMaxLockupPeriod", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "maxLockupPeriod", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requiredLockupPeriod", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientRateAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "rateAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rateUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumRateRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidChallengeCount", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minExpected", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidChallengeEpoch", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidChallengeWindowSize", + "inputs": [ + { + "name": "maxProvingPeriod", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeWindowSize", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidDataSetId", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidEpochRange", + "inputs": [ + { + "name": "fromEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "toEpoch", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidServiceDescriptionLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidServiceNameLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidTopUpAmount", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MaxProvingPeriodZero", + "inputs": [] + }, + { + "type": "error", + "name": "MetadataArrayCountMismatch", + "inputs": [ + { + "name": "metadataArrayCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceCount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MetadataKeyAndValueLengthMismatch", + "inputs": [ + { + "name": "keysLength", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "valuesLength", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MetadataKeyExceedsMaxLength", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "MetadataValueExceedsMaxLength", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NextProvingPeriodAlreadyCalled", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "periodDeadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nowBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NoPDPPaymentRail", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OnlyFilBeamControllerAllowed", + "inputs": [ + { + "name": "expected", + "type": "address", + "internalType": "address" + }, + { + "name": "actual", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OnlyPDPVerifierAllowed", + "inputs": [ + { + "name": "expected", + "type": "address", + "internalType": "address" + }, + { + "name": "actual", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OperatorNotApproved", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "PaymentRailsNotFinalized", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pdpEndEpoch", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "PriceExceedsMaximum", + "inputs": [ + { + "name": "priceType", + "type": "uint8", + "internalType": "enum Errors.PriceType" + }, + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actual", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProofAlreadySubmitted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderAlreadyApproved", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderIdMismatchAtIndex", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderNotInApprovedList", + "inputs": [ + { + "name": "providerId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProviderNotRegistered", + "inputs": [ + { + "name": "provider", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ProvingNotStarted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ProvingPeriodPassed", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nowBlock", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "RailNotAssociated", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "RailNotFullySettled", + "inputs": [ + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "settledUpTo", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "endEpoch", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ServiceContractMustTerminateRail", + "inputs": [] + }, + { + "type": "error", + "name": "StorageProviderChangesNotSupported", + "inputs": [] + }, + { + "type": "error", + "name": "TooManyMetadataKeys", + "inputs": [ + { + "name": "maxAllowed", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "keysLength", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [ + { + "name": "field", + "type": "uint8", + "internalType": "enum Errors.AddressField" + } + ] + } +] \ No newline at end of file diff --git a/apps/subgraph/abis/PDPService.json b/apps/subgraph/abis/PDPService.json new file mode 100644 index 00000000..b2ab5d30 --- /dev/null +++ b/apps/subgraph/abis/PDPService.json @@ -0,0 +1,649 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "NO_CHALLENGE_SCHEDULED", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NO_PROVING_DEADLINE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "challengeWindow", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "dataSetCreated", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "creator", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dataSetDeleted", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deletedLeafCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getChallengesPerProof", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "getMaxProvingPeriod", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "getPDPConfig", + "inputs": [], + "outputs": [ + { + "name": "maxProvingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "challengeWindow_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengesPerProof", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initChallengeWindowStart_", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initChallengeWindowStart", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "_pdpVerifierAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nextChallengeWindowStart", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nextPDPChallengeWindowStart", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nextProvingPeriod", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pdpVerifierAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "piecesAdded", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "firstAdded", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceData", + "type": "tuple[]", + "internalType": "struct Cids.Cid[]", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "piecesScheduledRemove", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "possessionProven", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "provenThisPeriod", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "provingDeadlines", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "storageProviderChanged", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "thisChallengeWindowStart", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "FaultRecord", + "inputs": [ + { + "name": "dataSetId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "periodsFaulted", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] diff --git a/apps/subgraph/abis/PDPVerifier.json b/apps/subgraph/abis/PDPVerifier.json new file mode 100644 index 00000000..6f7fb361 --- /dev/null +++ b/apps/subgraph/abis/PDPVerifier.json @@ -0,0 +1,1266 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "MAX_ENQUEUED_REMOVALS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_PIECE_SIZE_LOG2", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NO_CHALLENGE_SCHEDULED", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NO_PROVEN_EPOCH", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addPieces", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "listenerAddr", + "type": "address", + "internalType": "address" + }, + { + "name": "pieceData", + "type": "tuple[]", + "internalType": "struct Cids.Cid[]", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "calculateProofFee", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "calculateProofFeeForSize", + "inputs": [ + { + "name": "proofSize", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "claimDataSetStorageProvider", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "createDataSet", + "inputs": [ + { + "name": "listenerAddr", + "type": "address", + "internalType": "address" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "dataSetLive", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "deleteDataSet", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "feeEffectiveTime", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "feePerTiB", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint96", + "internalType": "uint96" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "findPieceIds", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "leafIndexs", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple[]", + "internalType": "struct IPDPTypes.PieceIdAndOffset[]", + "components": [ + { + "name": "pieceId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getActivePieceCount", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "activeCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getActivePieces", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "pieces", + "type": "tuple[]", + "internalType": "struct Cids.Cid[]", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "pieceIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "hasMore", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getChallengeFinality", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getChallengeRange", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDataSetLastProvenEpoch", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDataSetLeafCount", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDataSetListener", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDataSetStorageProvider", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNextChallengeEpoch", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNextDataSetId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNextPieceId", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPieceCid", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct Cids.Cid", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPieceLeafCount", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRandomness", + "inputs": [ + { + "name": "epoch", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getScheduledRemovals", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "_challengeFinality", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "migrate", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nextProvingPeriod", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "challengeEpoch", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pieceChallengable", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pieceLive", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proposeDataSetStorageProvider", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "newStorageProvider", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "proposedFeePerTiB", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint96", + "internalType": "uint96" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "provePossession", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proofs", + "type": "tuple[]", + "internalType": "struct IPDPTypes.Proof[]", + "components": [ + { + "name": "leaf", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "proof", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "schedulePieceDeletions", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "pieceIds", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "extraData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateProofFee", + "inputs": [ + { + "name": "newFeePerTiB", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "ContractUpgraded", + "inputs": [ + { + "name": "version", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "implementation", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetCreated", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "storageProvider", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetDeleted", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "deletedLeafCount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DataSetEmpty", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeUpdateProposed", + "inputs": [ + { + "name": "currentFee", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newFee", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "effectiveTime", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NextProvingPeriod", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "challengeEpoch", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "leafCount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PiecesAdded", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pieceIds", + "type": "uint256[]", + "indexed": false, + "internalType": "uint256[]" + }, + { + "name": "pieceCids", + "type": "tuple[]", + "indexed": false, + "internalType": "struct Cids.Cid[]", + "components": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PiecesRemoved", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "pieceIds", + "type": "uint256[]", + "indexed": false, + "internalType": "uint256[]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PossessionProven", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "challenges", + "type": "tuple[]", + "indexed": false, + "internalType": "struct IPDPTypes.PieceIdAndOffset[]", + "components": [ + { + "name": "pieceId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ProofFeePaid", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "fee", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "StorageProviderChanged", + "inputs": [ + { + "name": "setId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "oldStorageProvider", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newStorageProvider", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "IndexedError", + "inputs": [ + { + "name": "idx", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msg", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] diff --git a/apps/subgraph/networks.json b/apps/subgraph/networks.json new file mode 100644 index 00000000..93d77b0b --- /dev/null +++ b/apps/subgraph/networks.json @@ -0,0 +1,22 @@ +{ + "filecoin": { + "PDPVerifier": { + "address": "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F", + "startBlock": 5441432 + }, + "FilecoinWarmStorageService": { + "address": "0x8408502033C418E1bbC97cE9ac48E5528F371A9f", + "startBlock": 5459617 + } + }, + "filecoin-testnet": { + "PDPVerifier": { + "address": "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C", + "startBlock": 3140755 + }, + "FilecoinWarmStorageService": { + "address": "0x02925630df557F957f70E112bA06e50965417CA0", + "startBlock": 3141276 + } + } +} diff --git a/apps/subgraph/package.json b/apps/subgraph/package.json new file mode 100644 index 00000000..b71ad3ff --- /dev/null +++ b/apps/subgraph/package.json @@ -0,0 +1,21 @@ +{ + "name": "@dealbot/subgraph", + "private": true, + "license": "(MIT OR Apache-2.0)", + "scripts": { + "codegen": "graph codegen", + "build:mainnet": "graph codegen && graph build --network filecoin", + "build:calibnet": "graph codegen && graph build --network filecoin-testnet", + "deploy:mainnet": "goldsky subgraph deploy dealbot-subgraph/$VERSION --path ./build", + "deploy:calibnet": "goldsky subgraph deploy dealbot-subgraph-calibnet/$VERSION --path ./build", + "test": "graph test" + }, + "dependencies": { + "@graphprotocol/graph-cli": "0.98.1", + "@graphprotocol/graph-ts": "0.38.2" + }, + "devDependencies": { + "assemblyscript": "0.19.23", + "matchstick-as": "0.6.0" + } +} diff --git a/apps/subgraph/schema.graphql b/apps/subgraph/schema.graphql new file mode 100644 index 00000000..d842b80e --- /dev/null +++ b/apps/subgraph/schema.graphql @@ -0,0 +1,305 @@ +enum DataSetStatus { + EMPTY # Newly created dataset, no roots yet + READY # Dataset has roots and is ready for proving + PROVING # Proofs are currently expected for this dataset + DELETED # Dataset has been deleted on-chain +} + +type DataSet @entity(immutable: false) { + id: Bytes! # setId + setId: BigInt! # uint256 + listener: Service! # address + owner: Provider! # address of the provider + leafCount: BigInt! # uint256 + challengeRange: BigInt! # uint256 + # True iff the dataset has not been deleted (PDPVerifier.DataSetDeleted) + # and the FWSS service has not been terminated (FWSS.ServiceTerminated). + # Note: PDPPaymentTerminated does NOT affect this flag; use pdpPaymentEndEpoch. + isActive: Boolean! + status: DataSetStatus! + lastProvenEpoch: BigInt! # uint256 + nextChallengeEpoch: BigInt! # uint256 + # NOTE: Proving period tracking is strictly bound to FWSS contract + nextDeadline: BigInt! # Block number of next deadline (0 if not set) + firstDeadline: BigInt! # Block number of first NextProvingPeriod event (0 if not set) + maxProvingPeriod: BigInt! # Multiplier for proving period frequency (0 if not set) + challengeWindowSize: BigInt! # Size of challenge window before each deadline (0 if not set) + currentDeadlineCount: BigInt! # Current deadline count + provenThisPeriod: Boolean! + # Existing fields + totalRoots: BigInt! # uint256 + nextPieceId: BigInt! # uint256 + totalDataSize: BigInt! # uint256 + totalProofs: BigInt! # uint256 + totalProvedRoots: BigInt! # uint256 + totalFeePaid: BigInt! # uint256 + totalFaultedPeriods: BigInt! # uint256 + totalFaultedRoots: BigInt! # uint256 + totalTransactions: BigInt! # uint256 + totalEventLogs: BigInt! # uint256 + createdAt: BigInt! + updatedAt: BigInt! + blockNumber: BigInt! + + # ---- FWSS fields (null / empty for non-FWSS datasets) ---- + # Populated by FWSS.DataSetCreated handler. + fwssProviderId: BigInt # uint256 — FWSS numeric provider ID + fwssPayer: Bytes # address of the payer + fwssServiceProvider: Bytes # address — may diverge from owner after transfers + fwssPdpRailId: BigInt # uint256 — FWSS PDP rail ID + + # Raw metadata from DataSetCreated (kept for completeness; clients should + # prefer the derived booleans below for filter queries). + metadataKeys: [String!]! # empty array when not FWSS + metadataValues: [String!]! # empty array when not FWSS + + # Derived metadata flags (cheap to filter on). + withIPFSIndexing: Boolean! # true iff "withIPFSIndexing" in metadataKeys + withCDN: Boolean! # true iff "withCDN" in metadataKeys + + # Populated by FWSS.PDPPaymentTerminated handler. + # May be in the past (already terminated) or future (terminating). + # Does NOT flip isActive — clients that care must compare to current epoch. + pdpPaymentEndEpoch: BigInt + + # Derived relationships + roots: [Root!]! @derivedFrom(field: "proofSet") + transactions: [Transaction!]! @derivedFrom(field: "proofSet") + eventLogs: [EventLog!]! @derivedFrom(field: "proofSet") + faultRecords: [FaultRecord!]! @derivedFrom(field: "proofSet") + provingWindows: [ProvingWindow!]! @derivedFrom(field: "proofSet") + weeklyActivities: [WeeklyProofSetActivity!]! @derivedFrom(field: "proofSet") + monthlyActivities: [MonthlyProofSetActivity!]! @derivedFrom(field: "proofSet") +} + +type Service @entity(immutable: false) { + id: Bytes! # address + address: Bytes! # Service Contract Address + totalProofSets: BigInt! # uint256 + totalProviders: BigInt! # uint256 + totalRoots: BigInt! # uint256 + totalDataSize: BigInt! # uint256 + totalFaultedRoots: BigInt! + totalFaultedPeriods: BigInt! + createdAt: BigInt! + updatedAt: BigInt! + # Relationships + proofSets: [DataSet!]! @derivedFrom(field: "listener") + providerLinks: [ServiceProviderLink!]! @derivedFrom(field: "service") +} + +type ServiceProviderLink @entity(immutable: false) { + id: Bytes! + totalProofSets: BigInt! + # Relationships + service: Service! + provider: Provider! +} + +type Provider @entity(immutable: false) { + id: Bytes! # address + address: Bytes! + # Calculation of Faulted and total proving periods is strictly bound to FWSS + totalFaultedPeriods: BigInt! # total faulted periods recorded out of totalProvingPeriods + totalProvingPeriods: BigInt! # total proving periods recorded + totalFaultedRoots: BigInt! + totalProofSets: BigInt! + totalRoots: BigInt! + totalDataSize: BigInt! + createdAt: BigInt! + updatedAt: BigInt! + blockNumber: BigInt! + + # Derived relationship + proofSets: [DataSet!]! @derivedFrom(field: "owner") + serviceLinks: [ServiceProviderLink!]! @derivedFrom(field: "provider") + weeklyProviderActivities: [WeeklyProviderActivity!]! + @derivedFrom(field: "provider") + monthlyProviderActivities: [MonthlyProviderActivity!]! + @derivedFrom(field: "provider") +} + +type Root @entity(immutable: false) { + id: Bytes! # Unique ID for Root (e.g., setId-rootId) + setId: BigInt! # uint256 (Keep for filtering/direct access) + rootId: BigInt! # uint256 + rawSize: BigInt! # uint256 + leafCount: BigInt! # uint256 + cid: Bytes! + removed: Boolean! + totalProofsSubmitted: BigInt! # uint256 + totalPeriodsFaulted: BigInt! # uint256 + lastProvenEpoch: BigInt! # uint256 + lastProvenAt: BigInt! # uint256 + lastFaultedEpoch: BigInt! # uint256 + lastFaultedAt: BigInt! # uint256 + createdAt: BigInt! + updatedAt: BigInt! + blockNumber: BigInt! + + # ---- FWSS fields (null / empty for non-FWSS pieces) ---- + # Populated by FWSS.PieceAdded handler. + metadataKeys: [String!]! # empty array when not FWSS + metadataValues: [String!]! # empty array when not FWSS + ipfsRootCID: String # values[indexOf(keys, "ipfsRootCID")] or null + + # Relationship + proofSet: DataSet! # Link to DataSet (stores DataSet ID) + # Derived relationships + faultRecords: [FaultRecord!]! @derivedFrom(field: "roots") # For many-to-many derived +} + +type SumTreeCount @entity(immutable: false) { + id: Bytes! # setId-rootId + setId: BigInt! # uint256 (Keep for filtering/direct access) + rootId: BigInt! # uint256 + count: BigInt! # uint256 + lastCount: BigInt! # uint256 + lastDecEpoch: BigInt! +} + +type EventLog @entity(immutable: true) { + id: Bytes! # transactionHash-logIndex + setId: BigInt! # uint256 (Keep for filtering/direct access) + address: Bytes! + name: String! + data: String! + logIndex: BigInt! + transactionHash: Bytes! # Keep for linking + createdAt: BigInt! + blockNumber: BigInt! + + # Relationships + proofSet: DataSet! # Link to DataSet (stores DataSet ID) + transaction: Transaction! # Link to Transaction (stores Transaction hash) +} + +type Transaction @entity(immutable: true) { + id: Bytes! # hash + hash: Bytes! + dataSetId: BigInt! # uint256 (Keep for filtering/direct access) + height: BigInt! # uint256 + fromAddress: Bytes! # address + toAddress: Bytes # address + value: BigInt! # uint256 + method: String! + status: Boolean! + createdAt: BigInt! + + # Relationship + proofSet: DataSet! # Link to DataSet (stores DataSet ID) + # Derived relationship + eventLogs: [EventLog!]! @derivedFrom(field: "transaction") +} + +type FaultRecord @entity(immutable: true) { + id: Bytes! # Unique ID (e.g., txHash-logIndex) + dataSetId: BigInt! # uint256 (Keep for filtering) + pieceIds: [BigInt!]! # uint256[] (Keep for direct access) + currentChallengeEpoch: BigInt! # uint256 + nextChallengeEpoch: BigInt! # uint256 + periodsFaulted: BigInt! # uint256 + deadline: BigInt! # uint256 + createdAt: BigInt! + blockNumber: BigInt! + + # Relationships + proofSet: DataSet! # Link to DataSet (stores DataSet ID) + roots: [Root!]! # Link to Pieces (stores array of Root IDs) +} + +type ProvingWindow @entity(immutable: false) { + id: Bytes! # setId-deadlineCount + setId: BigInt! # uint256 + deadlineCount: BigInt! # Which deadline this represents + deadline: BigInt! # Block number of the deadline + windowStart: BigInt! # Block number when challenge window starts + windowEnd: BigInt! # Block number when challenge window ends (same as deadline) + proofSubmitted: Boolean! # Whether a valid proof was submitted in this window + proofBlockNumber: BigInt! # Block number when proof was submitted (0 if no proof) + isValid: Boolean! # Whether proof was submitted within the valid window + createdAt: BigInt! + + # Relationship + proofSet: DataSet! # Link to DataSet +} + +# Metrices + +type NetworkMetric @entity(immutable: false) { + id: Bytes! # Unique ID (e.g., txHash-logIndex) + totalProofSets: BigInt # uint256 + totalActiveProofSets: BigInt # uint256 + totalProviders: BigInt # uint256 + totalRoots: BigInt # uint256 + totalActiveRoots: BigInt # uint256 + totalDataSize: BigInt # uint256 + totalProofFeePaidInFil: BigInt # uint256 + totalProofs: BigInt # uint256 + totalProvedRoots: BigInt # uint256 + totalFaultedPeriods: BigInt # uint256 + totalFaultedRoots: BigInt # uint256 + totalServices: BigInt # uint256 +} + +type WeeklyProviderActivity @entity(immutable: false) { + id: Bytes! # Unique ID (e.g., time derived) + providerId: Bytes! # address (Keep for filtering) + totalRootsAdded: BigInt! # uint256 + totalDataSizeAdded: BigInt! # uint256 + totalRootsRemoved: BigInt! # uint256 + totalDataSizeRemoved: BigInt! # uint256 + totalProofSetsCreated: BigInt! # uint256 + totalProofs: BigInt! # uint256 + totalRootsProved: BigInt! # uint256 + totalFaultedRoots: BigInt! # uint256 + totalFaultedPeriods: BigInt! # uint256 + # Relationships + provider: Provider! # Link to Provider (stores Provider ID) +} + +type MonthlyProviderActivity @entity(immutable: false) { + id: Bytes! # Unique ID (e.g., time derived) + providerId: Bytes! # address (Keep for filtering) + totalProofSetsCreated: BigInt! # uint256 + totalRootsAdded: BigInt! # uint256 + totalDataSizeAdded: BigInt! # uint256 + totalRootsRemoved: BigInt! # uint256 + totalDataSizeRemoved: BigInt! # uint256 + totalProofs: BigInt! # uint256 + totalRootsProved: BigInt! # uint256 + totalFaultedRoots: BigInt! # uint256 + totalFaultedPeriods: BigInt! # uint256 + # Relationships + provider: Provider! # Link to Provider (stores Provider ID) +} + +type WeeklyProofSetActivity @entity(immutable: false) { + id: Bytes! # Unique ID (e.g., time derived) + dataSetId: BigInt! # uint256 (Keep for filtering) + totalRootsAdded: BigInt! # uint256 + totalDataSizeAdded: BigInt! # uint256 + totalRootsRemoved: BigInt! # uint256 + totalDataSizeRemoved: BigInt! # uint256 + totalProofs: BigInt! # uint256 + totalRootsProved: BigInt! # uint256 + totalFaultedRoots: BigInt! # uint256 + totalFaultedPeriods: BigInt! # uint256 + # Relationships + proofSet: DataSet! # Link to DataSet (stores DataSet ID) +} + +type MonthlyProofSetActivity @entity(immutable: false) { + id: Bytes! # Unique ID (e.g., time derived) + dataSetId: BigInt! # uint256 (Keep for filtering) + totalRootsAdded: BigInt! # uint256 + totalDataSizeAdded: BigInt! # uint256 + totalRootsRemoved: BigInt! # uint256 + totalDataSizeRemoved: BigInt! # uint256 + totalProofs: BigInt! # uint256 + totalRootsProved: BigInt! # uint256 + totalFaultedRoots: BigInt! # uint256 + totalFaultedPeriods: BigInt! # uint256 + # Relationships + proofSet: DataSet! # Link to DataSet (stores DataSet ID) +} diff --git a/apps/subgraph/src/fwss.ts b/apps/subgraph/src/fwss.ts new file mode 100644 index 00000000..983a151e --- /dev/null +++ b/apps/subgraph/src/fwss.ts @@ -0,0 +1,182 @@ +import { BigInt, Bytes, log } from "@graphprotocol/graph-ts"; +import { + DataSetCreated as DataSetCreatedEvent, + PieceAdded as PieceAddedEvent, + ServiceTerminated as ServiceTerminatedEvent, + PDPPaymentTerminated as PDPPaymentTerminatedEvent, + DataSetServiceProviderChanged as DataSetServiceProviderChangedEvent, +} from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService"; +import { DataSet, Root } from "../generated/schema"; +import { getRootEntityId } from "./pdp-verifier"; +import { saveNetworkMetrics } from "./helper"; + +// ---- Helpers -------------------------------------------------------------- + +function getProofSetEntityId(setId: BigInt): Bytes { + return Bytes.fromByteArray(Bytes.fromBigInt(setId)); +} + +function arrayContains(arr: string[], needle: string): boolean { + for (let i = 0; i < arr.length; i++) { + if (arr[i] == needle) return true; + } + return false; +} + +function extractMetadataValue( + keys: string[], + values: string[], + needle: string +): string | null { + for (let i = 0; i < keys.length; i++) { + if (keys[i] == needle) { + return i < values.length ? values[i] : null; + } + } + return null; +} + +// ---- Handlers ------------------------------------------------------------- + +export function handleFwssDataSetCreated(event: DataSetCreatedEvent): void { + const id = getProofSetEntityId(event.params.dataSetId); + // FWSS.DataSetCreated fires BEFORE PDPVerifier's own DataSetCreated event + // (see PDPVerifier._createDataSet: listener callback runs first, THEN + // `emit DataSetCreated`). If no entity exists yet, create a stub with + // required defaults; pdp-verifier.handleDataSetCreated will run later in + // the same block and fill in PDPVerifier-level fields. Since handlers run + // sequentially and atomically within a block, no GraphQL query can observe + // that intermediate state. + let ds = DataSet.load(id); + if (ds == null) { + ds = new DataSet(id); + ds.setId = event.params.dataSetId; + // PDPVerifier-level non-null fields — safe defaults; handleDataSetCreated + // will overwrite shortly after in this same block. + ds.owner = event.params.serviceProvider; + ds.listener = event.address; + ds.isActive = true; + ds.leafCount = BigInt.fromI32(0); + ds.challengeRange = BigInt.fromI32(0); + ds.lastProvenEpoch = BigInt.fromI32(0); + ds.nextChallengeEpoch = BigInt.fromI32(0); + ds.firstDeadline = BigInt.fromI32(0); + ds.maxProvingPeriod = BigInt.fromI32(0); + ds.challengeWindowSize = BigInt.fromI32(0); + ds.currentDeadlineCount = BigInt.fromI32(0); + ds.nextDeadline = BigInt.fromI32(0); + ds.provenThisPeriod = false; + ds.totalRoots = BigInt.fromI32(0); + ds.nextPieceId = BigInt.fromI32(0); + ds.totalDataSize = BigInt.fromI32(0); + ds.totalFeePaid = BigInt.fromI32(0); + ds.totalFaultedPeriods = BigInt.fromI32(0); + ds.totalFaultedRoots = BigInt.fromI32(0); + ds.totalProofs = BigInt.fromI32(0); + ds.totalProvedRoots = BigInt.fromI32(0); + ds.totalTransactions = BigInt.fromI32(0); + ds.totalEventLogs = BigInt.fromI32(0); + ds.createdAt = event.block.timestamp; + ds.blockNumber = event.block.number; + // status: EMPTY. Imported enum value would be cleaner, but schema.graphql + // defines the enum; matching literal is what the generated code stores. + ds.status = "EMPTY"; + } + + ds.fwssProviderId = event.params.providerId; + ds.fwssPayer = event.params.payer; + ds.fwssServiceProvider = event.params.serviceProvider; + ds.fwssPdpRailId = event.params.pdpRailId; + ds.metadataKeys = event.params.metadataKeys; + ds.metadataValues = event.params.metadataValues; + ds.withIPFSIndexing = arrayContains( + event.params.metadataKeys, + "withIPFSIndexing" + ); + ds.withCDN = arrayContains(event.params.metadataKeys, "withCDN"); + ds.updatedAt = event.block.timestamp; + ds.save(); +} + +export function handleFwssPieceAdded(event: PieceAddedEvent): void { + const id = getRootEntityId(event.params.dataSetId, event.params.pieceId); + const root = Root.load(id); + if (root == null) { + log.warning("FWSS PieceAdded for unknown root {}-{}", [ + event.params.dataSetId.toString(), + event.params.pieceId.toString(), + ]); + return; + } + + root.metadataKeys = event.params.keys; + root.metadataValues = event.params.values; + root.ipfsRootCID = extractMetadataValue( + event.params.keys, + event.params.values, + "ipfsRootCID" + ); + root.updatedAt = event.block.timestamp; + root.save(); +} + +export function handleFwssServiceTerminated( + event: ServiceTerminatedEvent +): void { + const id = getProofSetEntityId(event.params.dataSetId); + const ds = DataSet.load(id); + if (ds == null) { + log.warning("FWSS ServiceTerminated for unknown dataSet {}", [ + event.params.dataSetId.toString(), + ]); + return; + } + + // Guard against double-decrement of totalActiveProofSets in case both + // DataSetDeleted (PDPVerifier) and ServiceTerminated (FWSS) fire for the + // same dataset. Only decrement if this event is the one flipping isActive. + if (ds.isActive) { + saveNetworkMetrics( + ["totalActiveProofSets"], + [BigInt.fromI32(1)], + ["subtract"] + ); + } + ds.isActive = false; + ds.updatedAt = event.block.timestamp; + ds.save(); +} + +export function handleFwssPdpPaymentTerminated( + event: PDPPaymentTerminatedEvent +): void { + const id = getProofSetEntityId(event.params.dataSetId); + const ds = DataSet.load(id); + if (ds == null) { + log.warning("FWSS PDPPaymentTerminated for unknown dataSet {}", [ + event.params.dataSetId.toString(), + ]); + return; + } + + ds.pdpPaymentEndEpoch = event.params.endEpoch; + ds.updatedAt = event.block.timestamp; + ds.save(); +} + +export function handleFwssDataSetServiceProviderChanged( + event: DataSetServiceProviderChangedEvent +): void { + const id = getProofSetEntityId(event.params.dataSetId); + const ds = DataSet.load(id); + if (ds == null) { + log.warning("FWSS DataSetServiceProviderChanged for unknown dataSet {}", [ + event.params.dataSetId.toString(), + ]); + return; + } + + ds.fwssServiceProvider = event.params.newServiceProvider; + ds.updatedAt = event.block.timestamp; + ds.save(); +} diff --git a/apps/subgraph/src/helper.ts b/apps/subgraph/src/helper.ts new file mode 100644 index 00000000..ed8df5ef --- /dev/null +++ b/apps/subgraph/src/helper.ts @@ -0,0 +1,238 @@ +import { + BigInt, + Bytes, + Value, + log, + store, + Entity, +} from "@graphprotocol/graph-ts"; +import { NetworkMetric } from "../generated/schema"; + +export function saveNetworkMetrics( + keys: string[], + values: BigInt[], + methods?: string[] +): void { + const networkMetric = NetworkMetric.load(Bytes.fromUTF8("pdp_network_stats")); + + if (networkMetric) { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const method = methods ? methods[i] : "add"; + + const valueToChangeValue = networkMetric.get(key); + let valueToChange = BigInt.fromI32(0); + if (valueToChangeValue) { + valueToChange = valueToChangeValue.toBigInt(); + } + if (method == "add") { + networkMetric.set(key, Value.fromBigInt(valueToChange.plus(value))); + } else if (method == "subtract") { + networkMetric.set(key, Value.fromBigInt(valueToChange.minus(value))); + } else { + networkMetric.set(key, Value.fromBigInt(valueToChange.plus(value))); + } + } + networkMetric.save(); + } else { + const networkMetric = new NetworkMetric( + Bytes.fromUTF8("pdp_network_stats") + ); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const method = methods ? methods[i] : "add"; + + let valueToAdd = BigInt.fromI32(0); + if (method == "add") { + valueToAdd = value; + } else if (method == "subtract") { + valueToAdd = BigInt.fromI32(0); + } else { + valueToAdd = value; + } + + networkMetric.set(key, Value.fromBigInt(valueToAdd)); + } + networkMetric.save(); + } +} + +export function saveProviderMetrics( + entity: string, + id: Bytes, + providerId: Bytes, + keys: string[], + values: BigInt[], + methods?: string[] +): void { + const availableEntities = [ + "WeeklyProviderActivity", + "MonthlyProviderActivity", + ]; + if (!availableEntities.includes(entity)) { + log.error("Invalid entity: {}", [entity]); + return; + } + + const entityInstance = store.get(entity, id.toHexString()); + if (entityInstance) { + entityInstance.set("providerId", Value.fromBytes(providerId)); + entityInstance.set("provider", Value.fromBytes(providerId)); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const method = methods ? methods[i] : "add"; + + const valueToChangeValue = entityInstance.get(key); + let valueToChange = BigInt.fromI32(0); + if (valueToChangeValue) { + valueToChange = valueToChangeValue.toBigInt(); + } + if (method == "replace") { + entityInstance.set(key, Value.fromBigInt(value)); + } else if (method == "add") { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } else if (method == "subtract") { + entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); + } else { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } + } + store.set(entity, id.toHexString(), entityInstance); + } else { + let requiredKeys = [ + "totalRootsAdded", + "totalDataSizeAdded", + "totalProofSetsCreated", + "totalProofs", + "totalRootsProved", + "totalFaultedRoots", + "totalFaultedPeriods", + "totalRootsRemoved", + "totalDataSizeRemoved", + ]; + const entityInstance = new Entity(); + entityInstance.set("id", Value.fromBytes(id)); + entityInstance.set("providerId", Value.fromBytes(providerId)); + entityInstance.set("provider", Value.fromBytes(providerId)); + for (let i = 0; i < requiredKeys.length; i++) { + entityInstance.set(requiredKeys[i], Value.fromBigInt(BigInt.fromI32(0))); + } + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const method = methods ? methods[i] : "add"; + + const valueToChangeValue = entityInstance.get(key); + let valueToChange = BigInt.fromI32(0); + if (valueToChangeValue) { + valueToChange = valueToChangeValue.toBigInt(); + } + if (method == "replace") { + entityInstance.set(key, Value.fromBigInt(value)); + } else if (method == "add") { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } else if (method == "subtract") { + entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); + } else { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } + } + store.set(entity, id.toHexString(), entityInstance); + } +} + +export function saveProofSetMetrics( + entity: string, + id: Bytes, + dataSetId: BigInt, + keys: string[], + values: BigInt[], + methods?: string[] +): void { + const availableEntities = [ + "WeeklyProofSetActivity", + "MonthlyProofSetActivity", + ]; + if (!availableEntities.includes(entity)) { + log.error("Invalid entity: {}", [entity]); + return; + } + + const entityInstance = store.get(entity, id.toHexString()); + + if (entityInstance) { + entityInstance.set("dataSetId", Value.fromBigInt(dataSetId)); + entityInstance.set( + "proofSet", + Value.fromBytes(Bytes.fromByteArray(Bytes.fromBigInt(dataSetId))) + ); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const method = methods ? methods[i] : "add"; + + const valueToChangeValue = entityInstance.get(key); + let valueToChange = BigInt.fromI32(0); + if (valueToChangeValue) { + valueToChange = valueToChangeValue.toBigInt(); + } + if (method == "replace") { + entityInstance.set(key, Value.fromBigInt(value)); + } else if (method == "add") { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } else if (method == "subtract") { + entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); + } else { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } + } + store.set(entity, id.toHexString(), entityInstance); + } else { + let requiredKeys = [ + "totalRootsAdded", + "totalDataSizeAdded", + "totalProofs", + "totalRootsProved", + "totalFaultedRoots", + "totalFaultedPeriods", + "totalRootsRemoved", + "totalDataSizeRemoved", + ]; + const entityInstance = new Entity(); + entityInstance.set("id", Value.fromBytes(id)); + entityInstance.set("dataSetId", Value.fromBigInt(dataSetId)); + entityInstance.set( + "proofSet", + Value.fromBytes(Bytes.fromByteArray(Bytes.fromBigInt(dataSetId))) + ); + for (let i = 0; i < requiredKeys.length; i++) { + entityInstance.set(requiredKeys[i], Value.fromBigInt(BigInt.fromI32(0))); + } + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const method = methods ? methods[i] : "add"; + + const valueToChangeValue = entityInstance.get(key); + let valueToChange = BigInt.fromI32(0); + if (valueToChangeValue) { + valueToChange = valueToChangeValue.toBigInt(); + } + if (method == "replace") { + entityInstance.set(key, Value.fromBigInt(value)); + } else if (method == "add") { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } else if (method == "subtract") { + entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); + } else { + entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); + } + } + store.set(entity, id.toHexString(), entityInstance); + } +} diff --git a/apps/subgraph/src/pdp-service.ts b/apps/subgraph/src/pdp-service.ts new file mode 100644 index 00000000..bde3508b --- /dev/null +++ b/apps/subgraph/src/pdp-service.ts @@ -0,0 +1,371 @@ +import { BigInt, Bytes, crypto, Address, log } from "@graphprotocol/graph-ts"; +import { FaultRecord as FaultRecordEvent } from "../generated/PDPService/PDPService"; +import { PDPVerifier } from "../generated/PDPVerifier/PDPVerifier"; +import { PDPVerifierAddress, NumChallenges } from "../utils"; +import { + EventLog, + DataSet, + Provider, + FaultRecord, + Root, + Service, +} from "../generated/schema"; +import { + saveNetworkMetrics, + saveProviderMetrics, + saveProofSetMetrics, +} from "./helper"; +import { SumTree } from "./sumTree"; + +// --- Helper Functions +function getProofSetEntityId(setId: BigInt): Bytes { + return Bytes.fromByteArray(Bytes.fromBigInt(setId)); +} + +function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { + return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); +} + +function getTransactionEntityId(txHash: Bytes): Bytes { + return txHash; +} + +function getEventLogEntityId(txHash: Bytes, logIndex: BigInt): Bytes { + return txHash.concatI32(logIndex.toI32()); +} +// --- End Helper Functions + +/** + * Pads a Buffer or Uint8Array to 32 bytes with leading zeros. + */ +function padTo32Bytes(input: Uint8Array): Uint8Array { + if (input.length >= 32) return input; + const out = new Uint8Array(32); + out.set(input, 32 - input.length); + return out; +} + +/** + * Generates a deterministic challenge index using seed, proofSetID, proofIndex, and totalLeaves. + * Mirrors the logic from Go's generateChallengeIndex. + */ +export function generateChallengeIndex( + seed: Uint8Array, + proofSetID: BigInt, + proofIndex: i32, + totalLeaves: BigInt +): BigInt { + const data = new Uint8Array(32 + 32 + 8); + + const paddedSeed = padTo32Bytes(seed); + data.set(paddedSeed, 0); + + // Convert proofSetID to Bytes and pad to 32 bytes (Big-Endian padding implied by padTo32Bytes) + const psIDBytes = Bytes.fromBigInt(proofSetID); + const psIDPadded = padTo32Bytes(psIDBytes); + data.set(psIDPadded, 32); // Write 32 bytes at offset 32 + + // Convert proofIndex (i32) to an 8-byte Uint8Array (uint64 Big-Endian) + const idxBuf = new Uint8Array(8); // Create 8-byte buffer, initialized to zeros + idxBuf[7] = u8(proofIndex & 0xff); // Least significant byte + idxBuf[6] = u8((proofIndex >> 8) & 0xff); + idxBuf[5] = u8((proofIndex >> 16) & 0xff); + idxBuf[4] = u8((proofIndex >> 24) & 0xff); // Most significant byte of the i32 + + data.set(idxBuf, 64); // Write the 8 bytes at offset 64 + + const hashBytes = crypto.keccak256(Bytes.fromUint8Array(data)); + // hashBytes is big-endian, so expected to be reversed + const hashIntUnsignedR = BigInt.fromUnsignedBytes( + Bytes.fromUint8Array(Bytes.fromHexString(hashBytes.toHexString()).reverse()) + ); + + if (totalLeaves.isZero()) { + log.error( + "generateChallengeIndex: totalLeaves is zero, cannot calculate modulus. ProofSetID: {}. Seed: {}", + [proofSetID.toString(), Bytes.fromUint8Array(seed).toHex()] + ); + return BigInt.fromI32(0); + } + + const challengeIndex = hashIntUnsignedR.mod(totalLeaves); + return challengeIndex; +} + +export function ensureEvenHex(value: BigInt): string { + const hexRaw = value.toHex().slice(2); + let paddedHex = hexRaw; + if (hexRaw.length % 2 === 1) { + paddedHex = "0" + hexRaw; + } + return "0x" + paddedHex; +} + +export function findChallengedRoots( + dataSetId: BigInt, + nextPieceId: BigInt, + challengeEpoch: BigInt, + totalLeaves: BigInt, + blockNumber: BigInt +): BigInt[] { + const instance = PDPVerifier.bind( + Address.fromBytes(Bytes.fromHexString(PDPVerifierAddress)) + ); + + const seedIntResult = instance.try_getRandomness(challengeEpoch); + if (seedIntResult.reverted) { + log.warning("findChallengedRoots: Failed to get randomness for epoch {}", [ + challengeEpoch.toString(), + ]); + return []; + } + + const seedInt = seedIntResult.value; + const seedHex = ensureEvenHex(seedInt); + + const challenges: BigInt[] = []; + if (totalLeaves.isZero()) { + log.warning( + "findChallengedRoots: totalLeaves is zero for DataSet {}. Cannot generate challenges.", + [dataSetId.toString()] + ); + return []; + } + for (let i = 0; i < NumChallenges; i++) { + const leafIdx = generateChallengeIndex( + Bytes.fromHexString(seedHex), + dataSetId, + i32(i), + totalLeaves + ); + challenges.push(leafIdx); + } + + const sumTreeInstance = new SumTree(); + const pieceIds = sumTreeInstance.findPieceIds( + dataSetId.toI32(), + nextPieceId.toI32(), + challenges, + blockNumber + ); + if (!pieceIds) { + log.warning("findChallengedRoots: findPieceIds reverted for dataSetId {}", [ + dataSetId.toString(), + ]); + return []; + } + + const rootIdsArray: BigInt[] = []; + for (let i = 0; i < pieceIds.length; i++) { + rootIdsArray.push(pieceIds[i].rootId); + } + return rootIdsArray; +} + +// Updated Handler +export function handleFaultRecord(event: FaultRecordEvent): void { + const setId = event.params.dataSetId; + const periodsFaultedParam = event.params.periodsFaulted; + const proofSetEntityId = getProofSetEntityId(setId); + const entityId = getEventLogEntityId(event.transaction.hash, event.logIndex); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + const proofSet = DataSet.load(proofSetEntityId); + if (!proofSet) { + log.warning("handleFaultRecord: DataSet {} not found for event tx {}", [ + setId.toString(), + event.transaction.hash.toHex(), + ]); + return; + } + const challengeEpoch = proofSet.nextChallengeEpoch; + const challengeRange = proofSet.challengeRange; + const proofSetOwner = proofSet.owner; + const nextPieceId = proofSet.totalRoots; + + const eventLog = new EventLog(entityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "FaultRecord"; + eventLog.data = `{"dataSetId":"${setId.toString()}","periodsFaulted":"${periodsFaultedParam.toString()}","deadline":"${event.params.deadline.toString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + + let nextChallengeEpoch = BigInt.fromI32(0); + const inputData = event.transaction.input; + if (inputData.length >= 4 + 32) { + const potentialNextEpochBytes = inputData.slice(4 + 32, 4 + 32 + 32); + if (potentialNextEpochBytes.length == 32) { + // Convert reversed Uint8Array to Bytes before converting to BigInt + nextChallengeEpoch = BigInt.fromUnsignedBytes( + Bytes.fromUint8Array(potentialNextEpochBytes.reverse()) + ); + } + } else { + log.warning( + "handleFaultRecord: Transaction input data too short to parse potential nextChallengeEpoch.", + [] + ); + } + + const pieceIds = findChallengedRoots( + setId, + nextPieceId, + challengeEpoch, + challengeRange, + event.block.number + ); + + if (pieceIds.length === 0) { + log.info( + "handleFaultRecord: No roots found for challenge epoch {} in DataSet {}", + [challengeEpoch.toString(), setId.toString()] + ); + } + + let uniqueRootIds: BigInt[] = []; + let rootIdMap = new Map(); + for (let i = 0; i < pieceIds.length; i++) { + const rootIdStr = pieceIds[i].toString(); + if (!rootIdMap.has(rootIdStr)) { + uniqueRootIds.push(pieceIds[i]); + rootIdMap.set(rootIdStr, true); + } + } + + let rootEntityIds: Bytes[] = []; + for (let i = 0; i < uniqueRootIds.length; i++) { + const rootId = uniqueRootIds[i]; + const rootEntityId = getRootEntityId(setId, rootId); + + const root = Root.load(rootEntityId); + if (root) { + if (!root.lastFaultedEpoch.equals(challengeEpoch)) { + root.totalPeriodsFaulted = + root.totalPeriodsFaulted.plus(periodsFaultedParam); + } else { + log.info( + "handleFaultRecord: Root {} in Set {} already marked faulted for epoch {}", + [rootId.toString(), setId.toString(), challengeEpoch.toString()] + ); + } + root.lastFaultedEpoch = challengeEpoch; + root.lastFaultedAt = event.block.timestamp; + root.updatedAt = event.block.timestamp; + root.blockNumber = event.block.number; + root.save(); + } else { + log.warning( + "handleFaultRecord: Root {} for Set {} not found while recording fault", + [rootId.toString(), setId.toString()] + ); + } + rootEntityIds.push(rootEntityId); + } + + const faultRecord = new FaultRecord(entityId); + faultRecord.dataSetId = setId; + faultRecord.pieceIds = uniqueRootIds; + faultRecord.currentChallengeEpoch = challengeEpoch; + faultRecord.nextChallengeEpoch = nextChallengeEpoch; + faultRecord.periodsFaulted = periodsFaultedParam; + faultRecord.deadline = event.params.deadline; + faultRecord.createdAt = event.block.timestamp; + faultRecord.blockNumber = event.block.number; + + faultRecord.proofSet = proofSetEntityId; + faultRecord.roots = rootEntityIds; + + faultRecord.save(); + eventLog.save(); + + proofSet.totalFaultedPeriods = + proofSet.totalFaultedPeriods.plus(periodsFaultedParam); + proofSet.totalFaultedRoots = proofSet.totalFaultedRoots.plus( + BigInt.fromI32(uniqueRootIds.length) + ); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + + const provider = Provider.load(proofSetOwner); + if (provider) { + provider.totalFaultedPeriods = + provider.totalFaultedPeriods.plus(periodsFaultedParam); + provider.totalFaultedRoots = provider.totalFaultedRoots.plus( + BigInt.fromI32(uniqueRootIds.length) + ); + provider.updatedAt = event.block.timestamp; + provider.blockNumber = event.block.number; + provider.save(); + } else { + log.warning("handleFaultRecord: Provider {} not found for DataSet {}", [ + proofSetOwner.toHex(), + setId.toString(), + ]); + } + + // update Service stats + const service = Service.load(proofSet.listener); + if (service) { + service.totalFaultedPeriods = + service.totalFaultedPeriods.plus(periodsFaultedParam); + service.totalFaultedRoots = service.totalFaultedRoots.plus( + BigInt.fromI32(uniqueRootIds.length) + ); + service.updatedAt = event.block.number; + service.save(); + } + + // Update network metrics + const keys = ["totalFaultedPeriods", "totalFaultedRoots"]; + const values = [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)]; + const methods = ["add", "add"]; + saveNetworkMetrics(keys, values, methods); + + // Update provider and proof set metrics + const weekId = event.block.timestamp.toI32() / 604800; + const monthId = event.block.timestamp.toI32() / 2592000; + const providerId = proofSet.owner; + const weeklyProviderId = Bytes.fromI32(weekId).concat(providerId); + const monthlyProviderId = Bytes.fromI32(monthId).concat(providerId); + const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); + const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); + saveProviderMetrics( + "WeeklyProviderActivity", + weeklyProviderId, + providerId, + ["totalFaultedPeriods", "totalFaultedRoots"], + [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], + ["add", "add"] + ); + saveProviderMetrics( + "MonthlyProviderActivity", + monthlyProviderId, + providerId, + ["totalFaultedPeriods", "totalFaultedRoots"], + [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], + ["add", "add"] + ); + saveProofSetMetrics( + "WeeklyProofSetActivity", + weeklyProofSetId, + setId, + ["totalFaultedPeriods", "totalFaultedRoots"], + [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], + ["add", "add"] + ); + saveProofSetMetrics( + "MonthlyProofSetActivity", + monthlyProofSetId, + setId, + ["totalFaultedPeriods", "totalFaultedRoots"], + [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], + ["add", "add"] + ); +} diff --git a/apps/subgraph/src/pdp-verifier.ts b/apps/subgraph/src/pdp-verifier.ts new file mode 100644 index 00000000..2d687c3b --- /dev/null +++ b/apps/subgraph/src/pdp-verifier.ts @@ -0,0 +1,1603 @@ +import { BigInt, Bytes, log, store, Value } from "@graphprotocol/graph-ts"; +import { + NextProvingPeriod as NextProvingPeriodEvent, + PossessionProven as PossessionProvenEvent, + ProofFeePaid as ProofFeePaidEvent, + DataSetCreated as DataSetCreatedEvent, + DataSetDeleted as DataSetDeletedEvent, + DataSetEmpty as DataSetEmptyEvent, + StorageProviderChanged as StorageProviderChangedEvent, + PiecesAdded as PiecesAddedEvent, + PiecesRemoved as PiecesRemovedEvent, +} from "../generated/PDPVerifier/PDPVerifier"; +import { + EventLog, + Provider, + DataSet, + Root, + Transaction, + Service, + ServiceProviderLink, + ProvingWindow, +} from "../generated/schema"; +import { + saveProviderMetrics, + saveProofSetMetrics, + saveNetworkMetrics, +} from "./helper"; +import { SumTree } from "./sumTree"; +import { LeafSize, MaxProvingPeriod, ChallengeWindowSize, MaxProvingWindowsPerEvent } from "../utils"; +import { validateCommPv2, unpaddedSize } from "../utils/cid"; +import { DataSetStatus } from "./types"; + +// --- Helper Functions for ID Generation --- +function getProofSetEntityId(setId: BigInt): Bytes { + return Bytes.fromByteArray(Bytes.fromBigInt(setId)); +} + +export function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { + return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); +} + +function getServiceProviderLinkEntityId( + serviceAddr: Bytes, + providerAddr: Bytes +): Bytes { + return serviceAddr.concat(providerAddr); +} + +function getTransactionEntityId(txHash: Bytes): Bytes { + return txHash; +} + +function getEventLogEntityId(txHash: Bytes, logIndex: BigInt): Bytes { + return txHash.concatI32(logIndex.toI32()); +} + +// ----------------------------------------- + +export function handleDataSetCreated(event: DataSetCreatedEvent): void { + const listenerAddr = Bytes.fromUint8Array( + event.transaction.input.subarray(16, 36) + ); + + const proofSetEntityId = getProofSetEntityId(event.params.setId); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const providerEntityId = event.params.storageProvider; // Provider ID is the owner address + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = event.params.setId; // Keep raw ID for potential filtering + eventLog.address = event.address; + eventLog.name = "DataSetCreated"; + eventLog.data = `{"setId":"${event.params.setId.toString()}","storageProvider":"${event.params.storageProvider.toHexString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; // Keep raw hash + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Create Transaction + // Check if transaction already exists (e.g., from another log in the same tx) + let transaction = Transaction.load(transactionEntityId); + if (transaction == null) { + transaction = new Transaction(transactionEntityId); + transaction.hash = event.transaction.hash; + transaction.dataSetId = event.params.setId; // Keep raw ID for potential filtering + transaction.height = event.block.number; + transaction.fromAddress = event.transaction.from; + transaction.toAddress = event.transaction.to; // Can be null for contract creation + transaction.value = event.transaction.value; + transaction.method = "createDataSet"; // Or derive from input data if possible + transaction.status = true; // Assuming success if event emitted + transaction.createdAt = event.block.timestamp; + // Link entities + transaction.proofSet = proofSetEntityId; + transaction.save(); + } + + // Create or load DataSet. FWSS.dataSetCreated callback fires before + // PDPVerifier's own DataSetCreated event (see PDPVerifier._createDataSet: + // listener callback runs, THEN `emit DataSetCreated`). If the listener is + // FWSS, handleFwssDataSetCreated has already created a stub entity with + // FWSS-layer fields populated. Load to preserve those fields. + let proofSet = DataSet.load(proofSetEntityId); + if (proofSet == null) { + proofSet = new DataSet(proofSetEntityId); + // FWSS fields — defaulted on create; FWSS handler will overwrite if it fires later. + proofSet.metadataKeys = []; + proofSet.metadataValues = []; + proofSet.withIPFSIndexing = false; + proofSet.withCDN = false; + // fwssProviderId, fwssPayer, fwssServiceProvider, fwssPdpRailId, + // pdpPaymentEndEpoch are nullable — no init needed. + } + proofSet.setId = event.params.setId; + proofSet.owner = providerEntityId; // Link to Provider via owner address (which is Provider's ID) + proofSet.listener = listenerAddr; + proofSet.isActive = true; + proofSet.status = DataSetStatus.EMPTY; + proofSet.leafCount = BigInt.fromI32(0); + proofSet.challengeRange = BigInt.fromI32(0); + proofSet.lastProvenEpoch = BigInt.fromI32(0); + proofSet.nextChallengeEpoch = BigInt.fromI32(0); + // Initialize proving period tracking fields (will be set when first NextProvingPeriod is called) + proofSet.firstDeadline = BigInt.fromI32(0); + proofSet.maxProvingPeriod = BigInt.fromI32(0); + proofSet.challengeWindowSize = BigInt.fromI32(0); + proofSet.currentDeadlineCount = BigInt.fromI32(0); + proofSet.nextDeadline = BigInt.fromI32(0); + proofSet.provenThisPeriod = false; + // Existing fields + proofSet.totalRoots = BigInt.fromI32(0); + proofSet.nextPieceId = BigInt.fromI32(0); + proofSet.totalDataSize = BigInt.fromI32(0); + proofSet.totalFeePaid = BigInt.fromI32(0); + proofSet.totalFaultedPeriods = BigInt.fromI32(0); + proofSet.totalFaultedRoots = BigInt.fromI32(0); + proofSet.totalProofs = BigInt.fromI32(0); + proofSet.totalProvedRoots = BigInt.fromI32(0); + proofSet.totalTransactions = BigInt.fromI32(1); + proofSet.totalEventLogs = BigInt.fromI32(1); + proofSet.createdAt = event.block.timestamp; + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + + // network metrics variables + let network_totalProofSets = BigInt.fromI32(1); + let network_totalActiveProofSets = BigInt.fromI32(1); + let network_totalProviders = BigInt.fromI32(0); + let network_totalServices = BigInt.fromI32(0); + + // Create or Update Provider + let provider = Provider.load(providerEntityId); + if (provider == null) { + provider = new Provider(providerEntityId); + provider.address = event.params.storageProvider; + provider.totalRoots = BigInt.fromI32(0); + provider.totalProofSets = BigInt.fromI32(1); + provider.totalProvingPeriods = BigInt.fromI32(0); + provider.totalFaultedPeriods = BigInt.fromI32(0); + provider.totalFaultedRoots = BigInt.fromI32(0); + provider.totalDataSize = BigInt.fromI32(0); + provider.createdAt = event.block.timestamp; + provider.blockNumber = event.block.number; + + // update network metrics + network_totalProviders = BigInt.fromI32(1); + } else { + // Update timestamp/block even if exists + provider.totalProofSets = provider.totalProofSets.plus(BigInt.fromI32(1)); + provider.blockNumber = event.block.number; + } + // provider.proofSetIds = provider.proofSetIds.concat([event.params.setId]); // REMOVED - Handled by @derivedFrom + provider.updatedAt = event.block.timestamp; + provider.save(); + + // Store Service + let service = Service.load(listenerAddr); + if (service == null) { + service = new Service(listenerAddr); + service.address = listenerAddr; + service.totalProofSets = BigInt.fromI32(1); + service.totalProviders = BigInt.fromI32(0); + service.totalRoots = BigInt.fromI32(0); + service.totalDataSize = BigInt.fromI32(0); + service.totalFaultedPeriods = BigInt.fromI32(0); + service.totalFaultedRoots = BigInt.fromI32(0); + service.createdAt = event.block.timestamp; + + network_totalServices = BigInt.fromI32(1); + } else { + service.totalProofSets = service.totalProofSets.plus(BigInt.fromI32(1)); + } + service.updatedAt = event.block.timestamp; + + // Store ServiceProviderLink + let serviceProviderLink = ServiceProviderLink.load( + getServiceProviderLinkEntityId(listenerAddr, event.params.storageProvider) + ); + if (serviceProviderLink == null) { + serviceProviderLink = new ServiceProviderLink( + getServiceProviderLinkEntityId(listenerAddr, event.params.storageProvider) + ); + serviceProviderLink.totalProofSets = BigInt.fromI32(1); + serviceProviderLink.service = listenerAddr; + serviceProviderLink.provider = event.params.storageProvider; + + // update service stats + service.totalProviders = service.totalProviders.plus(BigInt.fromI32(1)); + } else { + serviceProviderLink.totalProofSets = + serviceProviderLink.totalProofSets.plus(BigInt.fromI32(1)); + } + service.save(); + serviceProviderLink.save(); + + // update network metrics + saveNetworkMetrics( + [ + "totalProofSets", + "totalActiveProofSets", + "totalProviders", + "totalServices", + ], + [ + network_totalProofSets, + network_totalActiveProofSets, + network_totalProviders, + network_totalServices, + ], + ["add", "add", "add", "add"] + ); + + // update provider and proof set metrics + const weekId = event.block.timestamp.toI32() / 604800; + const monthId = event.block.timestamp.toI32() / 2592000; + const weeklyProviderId = Bytes.fromI32(weekId).concat(providerEntityId); + const monthlyProviderId = Bytes.fromI32(monthId).concat(providerEntityId); + saveProviderMetrics( + "WeeklyProviderActivity", + weeklyProviderId, + providerEntityId, + ["totalProofSetsCreated"], + [BigInt.fromI32(1)], + ["add"] + ); + saveProviderMetrics( + "MonthlyProviderActivity", + monthlyProviderId, + providerEntityId, + ["totalProofSetsCreated"], + [BigInt.fromI32(1)], + ["add"] + ); +} + +export function handleDataSetDeleted(event: DataSetDeletedEvent): void { + saveNetworkMetrics( + ["totalActiveProofSets"], + [BigInt.fromI32(1)], + ["subtract"] + ); + const setId = event.params.setId; + const deletedLeafCount = event.params.deletedLeafCount; + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "DataSetDeleted"; + eventLog.data = `{"setId":"${setId.toString()}","deletedLeafCount":"${deletedLeafCount.toString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Create Transaction if it doesn't exist + let transaction = Transaction.load(transactionEntityId); + if (transaction == null) { + transaction = new Transaction(transactionEntityId); + transaction.hash = event.transaction.hash; + transaction.dataSetId = setId; + transaction.height = event.block.number; + transaction.fromAddress = event.transaction.from; + transaction.toAddress = event.transaction.to; + transaction.value = event.transaction.value; + transaction.method = "deleteDataSet"; // Example method name + transaction.status = true; + transaction.createdAt = event.block.timestamp; + transaction.proofSet = proofSetEntityId; // Link to DataSet + transaction.save(); + } + + // Load DataSet + const proofSet = DataSet.load(proofSetEntityId); + if (!proofSet) { + log.warning("DataSetDeleted: DataSet {} not found", [setId.toString()]); + return; + } + + const ownerAddress = proofSet.owner; + + // Load Provider (to update stats before changing owner) + const provider = Provider.load(ownerAddress); + if (provider) { + provider.totalDataSize = provider.totalDataSize.minus( + proofSet.totalDataSize + ); + if (provider.totalDataSize.lt(BigInt.fromI32(0))) { + provider.totalDataSize = BigInt.fromI32(0); + } + provider.totalProofSets = provider.totalProofSets.minus(BigInt.fromI32(1)); + provider.updatedAt = event.block.timestamp; + provider.blockNumber = event.block.number; + provider.save(); + } else { + log.warning("DataSetDeleted: Provider {} for DataSet {} not found", [ + ownerAddress.toHexString(), + setId.toString(), + ]); + } + + // Update DataSet + proofSet.isActive = false; + proofSet.status = DataSetStatus.DELETED; + proofSet.owner = Bytes.empty(); + proofSet.totalRoots = BigInt.fromI32(0); + proofSet.totalDataSize = BigInt.fromI32(0); + proofSet.nextChallengeEpoch = BigInt.fromI32(0); + proofSet.lastProvenEpoch = BigInt.fromI32(0); + proofSet.totalTransactions = proofSet.totalTransactions.plus( + BigInt.fromI32(1) + ); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + + // Note: Pieces associated with this DataSet are not automatically removed or updated here. + // They still exist but are linked to an inactive DataSet. + // Consider if Pieces should be marked as inactive or removed in handlePiecesRemoved if needed. +} + +export function handleStorageProviderChanged( + event: StorageProviderChangedEvent +): void { + const setId = event.params.setId; + const oldStorageProvider = event.params.oldStorageProvider; + const newStorageProvider = event.params.newStorageProvider; + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "StorageProviderChanged"; + eventLog.data = `{"setId":"${setId.toString()}","oldStorageProvider":"${oldStorageProvider.toHexString()}","newStorageProvider":"${newStorageProvider.toHexString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Create Transaction if it doesn't exist + let transaction = Transaction.load(transactionEntityId); + if (transaction == null) { + transaction = new Transaction(transactionEntityId); + transaction.hash = event.transaction.hash; + transaction.dataSetId = setId; + transaction.height = event.block.number; + transaction.fromAddress = event.transaction.from; + transaction.toAddress = event.transaction.to; + transaction.value = event.transaction.value; + transaction.method = "claimDataSetStorageProvider"; // Example method name + transaction.status = true; + transaction.createdAt = event.block.timestamp; + transaction.proofSet = proofSetEntityId; // Link to DataSet + transaction.save(); + } + + // Load DataSet + const proofSet = DataSet.load(proofSetEntityId); + if (!proofSet) { + log.warning("StorageProviderChanged: DataSet {} not found", [ + setId.toString(), + ]); + return; + } + + // Load Old Provider (if exists) - Just update timestamp, derived field handles removal + const oldProvider = Provider.load(oldStorageProvider); + if (oldProvider) { + oldProvider.totalProofSets = oldProvider.totalProofSets.minus( + BigInt.fromI32(1) + ); + oldProvider.updatedAt = event.block.timestamp; + oldProvider.blockNumber = event.block.number; + oldProvider.save(); + } else { + log.warning("StorageProviderChanged: Old Provider {} not found", [ + oldStorageProvider.toHexString(), + ]); + } + + // load old ServiceProvider link - check if totalProofSets > 1 or not + // if not delete entity else decrease totalProofSets + const oldServiceProviderLink = ServiceProviderLink.load( + getServiceProviderLinkEntityId(proofSet.listener, oldStorageProvider) + ); + if (oldServiceProviderLink) { + if (oldServiceProviderLink.totalProofSets.gt(BigInt.fromI32(1))) { + oldServiceProviderLink.totalProofSets = + oldServiceProviderLink.totalProofSets.minus(BigInt.fromI32(1)); + } else { + store.remove("ServiceProviderLink", oldServiceProviderLink.id.toString()); + } + oldServiceProviderLink.save(); + } + + // load new ServiceProvider link + let newServiceProviderLink = ServiceProviderLink.load( + getServiceProviderLinkEntityId(proofSet.listener, newStorageProvider) + ); + if (newServiceProviderLink) { + newServiceProviderLink.totalProofSets = + newServiceProviderLink.totalProofSets.plus(BigInt.fromI32(1)); + } else { + newServiceProviderLink = new ServiceProviderLink( + getServiceProviderLinkEntityId(proofSet.listener, newStorageProvider) + ); + newServiceProviderLink.totalProofSets = BigInt.fromI32(1); + newServiceProviderLink.service = proofSet.listener; + newServiceProviderLink.provider = newStorageProvider; + } + newServiceProviderLink.save(); + + // Load or Create New Provider - Just update timestamp/create, derived field handles addition + let newProvider = Provider.load(newStorageProvider); + if (newProvider == null) { + // update network metrics + saveNetworkMetrics(["totalProviders"], [BigInt.fromI32(1)], ["add"]); + newProvider = new Provider(newStorageProvider); + newProvider.address = newStorageProvider; + newProvider.totalRoots = BigInt.fromI32(0); + newProvider.totalFaultedPeriods = BigInt.fromI32(0); + newProvider.totalProvingPeriods = BigInt.fromI32(0); + newProvider.totalFaultedRoots = BigInt.fromI32(0); + newProvider.totalDataSize = BigInt.fromI32(0); + newProvider.totalProofSets = BigInt.fromI32(1); + newProvider.createdAt = event.block.timestamp; + newProvider.blockNumber = event.block.number; + } else { + newProvider.totalProofSets = newProvider.totalProofSets.plus( + BigInt.fromI32(1) + ); + newProvider.blockNumber = event.block.number; + } + newProvider.updatedAt = event.block.timestamp; + newProvider.save(); + + // Update DataSet Owner (this updates the derived relationship on both old and new Provider) + proofSet.owner = newStorageProvider; // Set owner to the new provider's ID + proofSet.totalTransactions = proofSet.totalTransactions.plus( + BigInt.fromI32(1) + ); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); +} + +export function handleProofFeePaid(event: ProofFeePaidEvent): void { + const setId = event.params.setId; + const fee = event.params.fee; + + // update network metrics + saveNetworkMetrics(["totalProofFeePaidInFil"], [fee], ["add"]); + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; // Keep raw ID + eventLog.address = event.address; + eventLog.name = "ProofFeePaid"; + eventLog.data = `{"dataSetId":"${setId.toString()}","fee":"${fee.toString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Update DataSet total fee paid + const proofSet = DataSet.load(proofSetEntityId); + if (proofSet) { + proofSet.totalFeePaid = proofSet.totalFeePaid.plus(fee); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + } else { + log.warning("ProofFeePaid: DataSet {} not found", [setId.toString()]); + } +} + +export function handleDataSetEmpty(event: DataSetEmptyEvent): void { + const setId = event.params.setId; + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "DataSetDeleted"; + eventLog.data = `{"setId":"${setId.toString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Update DataSet + const proofSet = DataSet.load(proofSetEntityId); + if (proofSet) { + const oldTotalDataSize = proofSet.totalDataSize; // Store size before zeroing + + proofSet.status = DataSetStatus.EMPTY; + proofSet.nextChallengeEpoch = BigInt.fromI32(0); + proofSet.lastProvenEpoch = BigInt.fromI32(0); + proofSet.totalRoots = BigInt.fromI32(0); + proofSet.totalDataSize = BigInt.fromI32(0); + proofSet.leafCount = BigInt.fromI32(0); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + + // Update Provider's total data size + const provider = Provider.load(proofSet.owner); + if (provider) { + // Subtract the size this proof set had *before* it was zeroed + provider.totalDataSize = provider.totalDataSize.minus(oldTotalDataSize); + if (provider.totalDataSize.lt(BigInt.fromI32(0))) { + provider.totalDataSize = BigInt.fromI32(0); // Prevent negative size + } + provider.updatedAt = event.block.timestamp; + provider.blockNumber = event.block.number; + provider.save(); + } else { + // It's possible the provider was deleted or owner changed before this event + log.warning("DataSetDeleted: Provider {} for DataSet {} not found", [ + proofSet.owner.toHexString(), + setId.toString(), + ]); + } + } else { + log.warning("DataSetDeleted: DataSet {} not found", [setId.toString()]); + } + // Note: This event implies all roots are gone. Existing Root entities + // linked to this DataSet might need to be marked as removed or deleted + // depending on the desired data retention policy. This handler doesn't do that. + // Consider adding logic here or in handlePiecesRemoved if needed. +} + +export function handlePossessionProven(event: PossessionProvenEvent): void { + const setId = event.params.setId; + const challenges = event.params.challenges; // Array of { rootId: BigInt, offset: BigInt } + const currentBlockNumber = event.block.number; // Use block number as epoch indicator + const currentTimestamp = event.block.timestamp; + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log (Only one per event, log all challenges) + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "PossessionProven"; + // Store challenges as a simple string representation for the log + let challengesStr = "["; + for (let i = 0; i < challenges.length; i++) { + challengesStr += `{"pieceId":${challenges[i].pieceId.toString()},"offset":${challenges[i].offset.toString()}}`; + if (i < challenges.length - 1) { + challengesStr += ","; + } + } + challengesStr += "]"; + eventLog.data = `{"setId":"${setId.toString()}","challenges":${challengesStr}}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = currentTimestamp; + eventLog.blockNumber = currentBlockNumber; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Create Transaction (if it doesn't exist) + let transaction = Transaction.load(transactionEntityId); + if (transaction == null) { + transaction = new Transaction(transactionEntityId); + transaction.hash = event.transaction.hash; + transaction.dataSetId = setId; // Keep raw ID + transaction.height = currentBlockNumber; + transaction.fromAddress = event.transaction.from; + transaction.toAddress = event.transaction.to; + transaction.value = event.transaction.value; + transaction.method = "provePossession"; // Example method name + transaction.status = true; + transaction.createdAt = currentTimestamp; + transaction.proofSet = proofSetEntityId; // Link to DataSet + transaction.save(); + } + + let uniqueRoots: BigInt[] = []; + let pieceIdMap = new Map(); + + // Process each challenge + for (let i = 0; i < challenges.length; i++) { + const challenge = challenges[i]; + const pieceId = challenge.pieceId; + + const pieceIdStr = pieceId.toString(); + if (!pieceIdMap.has(pieceIdStr)) { + uniqueRoots.push(pieceId); + pieceIdMap.set(pieceIdStr, true); + } + } + + for (let i = 0; i < uniqueRoots.length; i++) { + const rootId = uniqueRoots[i]; + const rootEntityId = getRootEntityId(setId, rootId); + const root = Root.load(rootEntityId); + if (root) { + root.lastProvenEpoch = currentBlockNumber; + root.lastProvenAt = currentTimestamp; + root.totalProofsSubmitted = root.totalProofsSubmitted.plus( + BigInt.fromI32(1) + ); + root.updatedAt = currentTimestamp; + root.blockNumber = currentBlockNumber; + root.save(); + } else { + log.warning( + "PossessionProven: Root {} for Set {} not found during challenge processing", + [rootId.toString(), setId.toString()] + ); + } + } + + // Update DataSet (once per event) + const proofSet = DataSet.load(proofSetEntityId); + if (proofSet) { + const deadlineCount = proofSet.currentDeadlineCount; + + // Update existing proving window + const provingWindowId = Bytes.fromUTF8( + setId.toString() + "-" + deadlineCount.toString() + ); + const currentProvingWindow = ProvingWindow.load(provingWindowId); + + if (currentProvingWindow) { + currentProvingWindow.proofSubmitted = true; + currentProvingWindow.proofBlockNumber = currentBlockNumber; + currentProvingWindow.isValid = true; + currentProvingWindow.save(); + } else { + log.warning( + "PossessionProven: proving window not found for set {} and deadline count {}", + [setId.toString(), deadlineCount.toString()] + ); + } + + proofSet.lastProvenEpoch = currentBlockNumber; // Update last proven epoch for the set + proofSet.provenThisPeriod = true; // Mark that proof was submitted this period + proofSet.totalProvedRoots = proofSet.totalProvedRoots.plus( + BigInt.fromI32(uniqueRoots.length) + ); + proofSet.totalProofs = proofSet.totalProofs.plus(BigInt.fromI32(1)); + proofSet.totalTransactions = proofSet.totalTransactions.plus( + BigInt.fromI32(1) + ); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = currentTimestamp; + proofSet.blockNumber = currentBlockNumber; + proofSet.save(); + + // update provider and proof set metrics + const weekId = currentTimestamp.toI32() / 604800; + const monthId = currentTimestamp.toI32() / 2592000; + const providerAddr = proofSet.owner; + const weeklyProviderId = Bytes.fromI32(weekId).concat(providerAddr); + const monthlyProviderId = Bytes.fromI32(monthId).concat(providerAddr); + const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); + const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); + + saveProviderMetrics( + "WeeklyProviderActivity", + weeklyProviderId, + providerAddr, + ["totalProofs", "totalRootsProved"], + [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], + ["add", "add"] + ); + saveProviderMetrics( + "MonthlyProviderActivity", + monthlyProviderId, + providerAddr, + ["totalProofs", "totalRootsProved"], + [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], + ["add", "add"] + ); + saveProofSetMetrics( + "WeeklyProofSetActivity", + weeklyProofSetId, + setId, + ["totalProofs", "totalRootsProved"], + [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], + ["add", "add"] + ); + saveProofSetMetrics( + "MonthlyProofSetActivity", + monthlyProofSetId, + setId, + ["totalProofs", "totalRootsProved"], + [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], + ["add", "add"] + ); + } else { + log.warning("PossessionProven: DataSet {} not found", [setId.toString()]); + } + + // Update network metrics + saveNetworkMetrics( + ["totalProvedRoots", "totalProofs"], + [BigInt.fromI32(uniqueRoots.length), BigInt.fromI32(1)], + ["add", "add"] + ); +} + +export function handleNextProvingPeriod(event: NextProvingPeriodEvent): void { + const setId = event.params.setId; + const challengeEpoch = event.params.challengeEpoch; + const leafCount = event.params.leafCount; + const currentTimestamp = event.block.timestamp; + const currentBlockNumber = event.block.number; + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "NextProvingPeriod"; + eventLog.data = `{"setId":"${setId.toString()}","challengeEpoch":"${challengeEpoch.toString()}","leafCount":"${leafCount.toString()}"}`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = currentTimestamp; + eventLog.blockNumber = currentBlockNumber; + // Link entities + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Create Transaction (if it doesn't exist) + let transaction = Transaction.load(transactionEntityId); + if (transaction == null) { + transaction = new Transaction(transactionEntityId); + transaction.hash = event.transaction.hash; + transaction.dataSetId = setId; + transaction.height = event.block.number; + transaction.fromAddress = event.transaction.from; + transaction.toAddress = event.transaction.to; + transaction.value = event.transaction.value; + transaction.method = "nextProvingPeriod"; // Example method name + transaction.status = true; + transaction.createdAt = currentTimestamp; + transaction.proofSet = proofSetEntityId; // Link to DataSet + transaction.save(); + } + + // Update Data Set + const proofSet = DataSet.load(proofSetEntityId); + if (proofSet) { + let periodsSkipped: BigInt = BigInt.zero(); + let faultedPeriods: BigInt = BigInt.zero(); + let nextDeadline: BigInt; + + // initialize state for new data set + if (proofSet.nextDeadline.equals(BigInt.fromI32(0))) { + proofSet.status = DataSetStatus.PROVING; + proofSet.firstDeadline = currentBlockNumber; + // Set default values for proving period configuration. + // Modify MaxProvingPeriod / ChallengeWindowSize in utils/index.ts for each network. + proofSet.maxProvingPeriod = BigInt.fromI32(MaxProvingPeriod); + proofSet.challengeWindowSize = BigInt.fromI32(ChallengeWindowSize); + nextDeadline = currentBlockNumber.plus(proofSet.maxProvingPeriod); + } else { + if (currentBlockNumber.gt(proofSet.nextDeadline)) + periodsSkipped = currentBlockNumber + .minus(proofSet.nextDeadline.plus(BigInt.fromI32(1))) + .div(proofSet.maxProvingPeriod); + + nextDeadline = proofSet.nextDeadline.plus( + proofSet.maxProvingPeriod.times(periodsSkipped.plus(BigInt.fromI32(1))) + ); + faultedPeriods = proofSet.provenThisPeriod + ? periodsSkipped + : periodsSkipped.plus(BigInt.fromI32(1)); + } + + // Create ProvingWindow entity for skipped and current period. + // Cap the number of entities created per event to avoid OOM when syncing stale datasets + // that have accumulated many skipped periods (periodsSkipped can be tens of thousands). + // Fault counts are tracked accurately in faultedPeriods regardless of this cap. + const provingWindows = proofSet.leafCount.equals(BigInt.fromI32(0)) + ? periodsSkipped + : periodsSkipped.plus(BigInt.fromI32(1)); + const windowCap = BigInt.fromI32(MaxProvingWindowsPerEvent); + const windowStart = provingWindows.gt(windowCap) + ? provingWindows.minus(windowCap) + : BigInt.fromI32(0); + for (let i = windowStart.toI64(); i < provingWindows.toI64(); i++) { + const deadlineCount = proofSet.currentDeadlineCount + .plus(BigInt.fromI32(1)) + .plus(BigInt.fromI64(i)); + const periodDeadline = proofSet.firstDeadline.plus( + deadlineCount.times(proofSet.maxProvingPeriod) + ); + const provingWindowId = Bytes.fromUTF8( + setId.toString() + "-" + deadlineCount.toString() + ); + let provingWindow = new ProvingWindow(provingWindowId); + provingWindow.setId = setId; + provingWindow.deadlineCount = deadlineCount; + provingWindow.deadline = periodDeadline; + provingWindow.windowStart = periodDeadline.minus( + proofSet.challengeWindowSize + ); + provingWindow.windowEnd = periodDeadline; + provingWindow.proofSubmitted = false; + provingWindow.proofBlockNumber = BigInt.fromI32(0); + provingWindow.isValid = false; + provingWindow.createdAt = currentTimestamp; + provingWindow.proofSet = proofSetEntityId; + provingWindow.save(); + } + + proofSet.nextDeadline = nextDeadline; + proofSet.nextChallengeEpoch = challengeEpoch; + proofSet.challengeRange = leafCount; + proofSet.currentDeadlineCount = proofSet.currentDeadlineCount.plus( + periodsSkipped.plus(BigInt.fromI32(1)) + ); + proofSet.provenThisPeriod = false; + proofSet.totalFaultedPeriods = + proofSet.totalFaultedPeriods.plus(faultedPeriods); + proofSet.totalTransactions = proofSet.totalTransactions.plus( + BigInt.fromI32(1) + ); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = currentTimestamp; + proofSet.blockNumber = currentBlockNumber; + + // Check if data set is empty + if (proofSet.leafCount.equals(BigInt.fromI32(0))) { + proofSet.nextDeadline = BigInt.fromI32(0); + proofSet.nextChallengeEpoch = BigInt.fromI32(0); + proofSet.firstDeadline = BigInt.fromI32(0); + proofSet.maxProvingPeriod = BigInt.fromI32(0); + proofSet.challengeWindowSize = BigInt.fromI32(0); + proofSet.currentDeadlineCount = BigInt.fromI32(0); + } + + proofSet.save(); + + const provider = Provider.load(proofSet.owner); + if (provider) { + provider.totalFaultedPeriods = + provider.totalFaultedPeriods.plus(faultedPeriods); + provider.totalProvingPeriods = provider.totalProvingPeriods.plus( + periodsSkipped.plus(BigInt.fromI32(1)) + ); + provider.updatedAt = currentTimestamp; + provider.blockNumber = currentBlockNumber; + provider.save(); + } + + // update provider and proof set metrics + const weekId = currentTimestamp.toI32() / 604800; + const monthId = currentTimestamp.toI32() / 2592000; + const providerAddr = proofSet.owner; + const weeklyProviderId = Bytes.fromI32(weekId).concat(providerAddr); + const monthlyProviderId = Bytes.fromI32(monthId).concat(providerAddr); + const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); + const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); + + // Five challenges are supposed to be proven by an SP in an proving period + // Assuming that each challenge is for a unique root + const faultedRoots = faultedPeriods.times(BigInt.fromI32(5)); + + saveProviderMetrics( + "WeeklyProviderActivity", + weeklyProviderId, + providerAddr, + ["totalFaultedPeriods", "totalFaultedRoots"], + [faultedPeriods, faultedRoots], + ["add", "add"] + ); + saveProviderMetrics( + "MonthlyProviderActivity", + monthlyProviderId, + providerAddr, + ["totalFaultedPeriods", "totalFaultedRoots"], + [faultedPeriods, faultedRoots], + ["add", "add"] + ); + saveProofSetMetrics( + "WeeklyProofSetActivity", + weeklyProofSetId, + setId, + ["totalFaultedPeriods", "totalFaultedRoots"], + [faultedPeriods, faultedRoots], + ["add", "add"] + ); + saveProofSetMetrics( + "MonthlyProofSetActivity", + monthlyProofSetId, + setId, + ["totalFaultedPeriods", "totalFaultedRoots"], + [faultedPeriods, faultedRoots], + ["add", "add"] + ); + + // update network metrics + saveNetworkMetrics( + ["totalFaultedPeriods", "totalFaultedRoots"], + [faultedPeriods, faultedRoots], + ["add", "add"] + ); + + // update Service Metrics + const service = Service.load(proofSet.listener); + if (service) { + service.totalFaultedPeriods = + service.totalFaultedPeriods.plus(faultedPeriods); + service.totalFaultedRoots = service.totalFaultedRoots.plus(faultedRoots); + service.updatedAt = currentTimestamp; + service.save(); + } + } else { + log.warning("NextProvingPeriod: DataSet {} not found", [setId.toString()]); + } +} + +export function handlePiecesAdded(event: PiecesAddedEvent): void { + const setId = event.params.setId; + const rootIdsFromEvent = event.params.pieceIds; // Get root IDs from event params + const pieceCidsFromEvent = event.params.pieceCids; + + // Input parsing is necessary to get rawSize and root bytes (cid) + const txInput = event.transaction.input; + + if (txInput.length < 4) { + log.error("Invalid tx input length in handlePiecesAdded: {}", [ + event.transaction.hash.toHex(), + ]); + return; + } + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "piecesAdded"; + // Store simple representation of event params + let pieceIdStrings: string[] = []; + for (let i = 0; i < rootIdsFromEvent.length; i++) { + pieceIdStrings.push(rootIdsFromEvent[i].toString()); + } + eventLog.data = `{ "setId": "${setId.toString()}", "pieceIds": [${pieceIdStrings.join(",")}] }`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Create Transaction (if it doesn't exist) + let transaction = Transaction.load(transactionEntityId); + if (transaction == null) { + transaction = new Transaction(transactionEntityId); + transaction.hash = event.transaction.hash; + transaction.dataSetId = setId; + transaction.height = event.block.number; + transaction.fromAddress = event.transaction.from; + const toAddress = event.transaction.to; + if (toAddress) { + transaction.toAddress = toAddress; + } + transaction.value = event.transaction.value; + transaction.method = "addPieces"; // Example method name + transaction.status = true; + transaction.createdAt = event.block.timestamp; + transaction.proofSet = proofSetEntityId; + transaction.save(); + } + + // Load DataSet + const proofSet = DataSet.load(proofSetEntityId); + if (!proofSet) { + log.warning("handlePiecesAdded: DataSet {} not found for event tx {}", [ + setId.toString(), + event.transaction.hash.toHex(), + ]); + return; + } + + // --- Parse Transaction Input --- Requires helper functions + // Skip function selector (first 4 bytes) + const encodedData = Bytes.fromUint8Array(txInput.slice(4)); + + // Decode setId (uint256 at offset 0) + let decodedSetId: BigInt = readUint256(encodedData, 0); + if (decodedSetId != setId) { + log.warning( + "Decoded setId {} does not match event param {} in handlePiecesAdded. Tx: {}. Using event param.", + [ + decodedSetId.toString(), + setId.toString(), + event.transaction.hash.toHex(), + ] + ); + } + + // Decode rootsData (tuple[]) + let rootsDataOffset = readUint256(encodedData, 64).toI32(); // Offset is at byte 32 + let rootsDataLength: i32; + + if (rootsDataOffset < 0 || encodedData.length < rootsDataOffset + 32) { + log.error( + "handlePiecesAdded: Invalid rootsDataOffset {} or data length {} for reading rootsData length. Tx: {}", + [ + rootsDataOffset.toString(), + encodedData.length.toString(), + event.transaction.hash.toHex(), + ] + ); + return; + } + + rootsDataLength = readUint256(encodedData, rootsDataOffset).toI32(); // Length is at the offset + + if (rootsDataLength < 0) { + log.error( + "handlePiecesAdded: Invalid negative rootsDataLength {}. Tx: {}", + [rootsDataLength.toString(), event.transaction.hash.toHex()] + ); + return; + } + + // Check if number of roots from input matches event param + if (rootsDataLength != rootIdsFromEvent.length) { + log.error( + "handlePiecesAdded: Decoded roots count ({}) does not match event param count ({}). Tx: {}", + [ + rootsDataLength.toString(), + rootIdsFromEvent.length.toString(), + event.transaction.hash.toHex(), + ] + ); + // Decide how to proceed. For now, use the event length as the source of truth for iteration. + rootsDataLength = rootIdsFromEvent.length; + } + + let addedRootCount = 0; + let totalDataSizeAdded = BigInt.fromI32(0); + + // Create Root entities + const structsBaseOffset = rootsDataOffset + 32; // Start of struct offsets/data + + for (let i = 0; i < rootsDataLength; i++) { + const rootId = rootIdsFromEvent[i]; // Use rootId from event params + const pieceCid = pieceCidsFromEvent[i]; + + // Calculate offset for this struct's data + const structDataRelOffset = readUint256( + encodedData, + structsBaseOffset + i * 32 + ).toI32(); + const structDataAbsOffset = rootsDataOffset + 32 + structDataRelOffset; // Correct absolute offset + + // Check bounds for reading struct content (root offset + rawSize) + if ( + structDataAbsOffset < 0 || + encodedData.length < structDataAbsOffset + 64 + ) { + log.error( + "handlePiecesAdded: Encoded data too short or invalid offset for root struct content. Index: {}, Offset: {}, Len: {}. Tx: {}", + [ + i.toString(), + structDataAbsOffset.toString(), + encodedData.length.toString(), + event.transaction.hash.toHex(), + ] + ); + continue; // Skip this root + } + + const pieceBytes = pieceCid.data; + const commPData = validateCommPv2(pieceBytes); + const rawSize = commPData.isValid + ? unpaddedSize(commPData.padding, commPData.height) + : BigInt.zero(); + + const rootEntityId = getRootEntityId(setId, rootId); + + let root = Root.load(rootEntityId); + if (root) { + log.warning( + "handlePiecesAdded: Root {} for Set {} already exists. This shouldn't happen. Skipping.", + [rootId.toString(), setId.toString()] + ); + continue; + } + + root = new Root(rootEntityId); + root.rootId = rootId; + root.setId = setId; + root.rawSize = rawSize; // Use correct field name + root.leafCount = rawSize.div(BigInt.fromI32(LeafSize)); + root.cid = pieceCid.data; // Use correct field name + root.removed = false; // Explicitly set removed to false + root.lastProvenEpoch = BigInt.fromI32(0); + root.lastProvenAt = BigInt.fromI32(0); + root.lastFaultedEpoch = BigInt.fromI32(0); + root.lastFaultedAt = BigInt.fromI32(0); + root.totalProofsSubmitted = BigInt.fromI32(0); + root.totalPeriodsFaulted = BigInt.fromI32(0); + root.createdAt = event.block.timestamp; + root.updatedAt = event.block.timestamp; + root.blockNumber = event.block.number; + root.proofSet = proofSetEntityId; // Link to DataSet + // FWSS fields — defaulted here; patched in FWSS handler if applicable. + root.metadataKeys = []; + root.metadataValues = []; + // ipfsRootCID is nullable — no init needed. + + root.save(); + + // Update SumTree + const sumTree = new SumTree(); + sumTree.sumTreeAdd( + setId.toI32(), + rawSize.div(BigInt.fromI32(LeafSize)), + rootId.toI32() + ); + + addedRootCount += 1; + totalDataSizeAdded = totalDataSizeAdded.plus(rawSize); + } + + // Update DataSet stats + const previousDataSize = proofSet.totalDataSize; + if (previousDataSize.equals(BigInt.zero())) { + // First piece added, mark as ready for proving + // status will change to PROVING in nextProvingPeriod call + proofSet.status = DataSetStatus.READY; + } + proofSet.totalRoots = proofSet.totalRoots.plus( + BigInt.fromI32(addedRootCount) + ); + proofSet.nextPieceId = proofSet.nextPieceId.plus( + BigInt.fromI32(addedRootCount) + ); + proofSet.totalDataSize = proofSet.totalDataSize.plus(totalDataSizeAdded); + proofSet.leafCount = proofSet.leafCount.plus( + totalDataSizeAdded.div(BigInt.fromI32(LeafSize)) + ); + proofSet.totalTransactions = proofSet.totalTransactions.plus( + BigInt.fromI32(1) + ); + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + + // Update Provider stats + const provider = Provider.load(proofSet.owner); + if (provider) { + provider.totalDataSize = provider.totalDataSize.plus(totalDataSizeAdded); + provider.totalRoots = provider.totalRoots.plus( + BigInt.fromI32(addedRootCount) + ); + provider.updatedAt = event.block.timestamp; + provider.blockNumber = event.block.number; + provider.save(); + } else { + log.warning("handlePiecesAdded: Provider {} for DataSet {} not found", [ + proofSet.owner.toHex(), + setId.toString(), + ]); + } + + // update Service stats + const service = Service.load(proofSet.listener); + if (service) { + service.totalRoots = service.totalRoots.plus( + BigInt.fromI32(addedRootCount) + ); + service.totalDataSize = service.totalDataSize.plus(totalDataSizeAdded); + service.updatedAt = event.block.number; + service.save(); + } + + // Update network metrics + saveNetworkMetrics( + ["totalRoots", "totalActiveRoots", "totalDataSize"], + [ + BigInt.fromI32(addedRootCount), + BigInt.fromI32(addedRootCount), + totalDataSizeAdded, + ], + ["add", "add", "add"] + ); + + // update provider and proof set metrics + const weekId = event.block.timestamp.toI32() / 604800; + const monthId = event.block.timestamp.toI32() / 2592000; + const providerId = proofSet.owner; + const weeklyProviderId = Bytes.fromI32(weekId).concat(providerId); + const monthlyProviderId = Bytes.fromI32(monthId).concat(providerId); + const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); + const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); + saveProviderMetrics( + "WeeklyProviderActivity", + weeklyProviderId, + providerId, + ["totalRootsAdded", "totalDataSizeAdded"], + [BigInt.fromI32(addedRootCount), totalDataSizeAdded], + ["add", "add"] + ); + saveProviderMetrics( + "MonthlyProviderActivity", + monthlyProviderId, + providerId, + ["totalRootsAdded", "totalDataSizeAdded"], + [BigInt.fromI32(addedRootCount), totalDataSizeAdded], + ["add", "add"] + ); + saveProofSetMetrics( + "WeeklyProofSetActivity", + weeklyProofSetId, + setId, + ["totalRootsAdded", "totalDataSizeAdded"], + [BigInt.fromI32(addedRootCount), totalDataSizeAdded], + ["add", "add"] + ); + saveProofSetMetrics( + "MonthlyProofSetActivity", + monthlyProofSetId, + setId, + ["totalRootsAdded", "totalDataSizeAdded"], + [BigInt.fromI32(addedRootCount), totalDataSizeAdded], + ["add", "add"] + ); +} + +export function handlePiecesRemoved(event: PiecesRemovedEvent): void { + const setId = event.params.setId; + const pieceIds = event.params.pieceIds; + + const proofSetEntityId = getProofSetEntityId(setId); + const eventLogEntityId = getEventLogEntityId( + event.transaction.hash, + event.logIndex + ); + const transactionEntityId = getTransactionEntityId(event.transaction.hash); + + // Create Event Log + const eventLog = new EventLog(eventLogEntityId); + eventLog.setId = setId; + eventLog.address = event.address; + eventLog.name = "PiecesRemoved"; + // Store simple representation of event params + let removedRootIdStrings: string[] = []; + for (let i = 0; i < pieceIds.length; i++) { + removedRootIdStrings.push(pieceIds[i].toString()); + } + eventLog.data = `{ "setId": "${setId.toString()}", "pieceIds": [${removedRootIdStrings.join(",")}] }`; + eventLog.logIndex = event.logIndex; + eventLog.transactionHash = event.transaction.hash; + eventLog.createdAt = event.block.timestamp; + eventLog.blockNumber = event.block.number; + eventLog.proofSet = proofSetEntityId; + eventLog.transaction = transactionEntityId; + eventLog.save(); + + // Load DataSet + const proofSet = DataSet.load(proofSetEntityId); + if (!proofSet) { + log.warning("handlePiecesRemoved: DataSet {} not found for event tx {}", [ + setId.toString(), + event.transaction.hash.toHex(), + ]); + return; + } + + let removedRootCount = 0; + let removedDataSize = BigInt.fromI32(0); + + // Mark Root entities as removed (soft delete) + for (let i = 0; i < pieceIds.length; i++) { + const rootId = pieceIds[i]; + const rootEntityId = getRootEntityId(setId, rootId); + + const root = Root.load(rootEntityId); + if (root) { + removedRootCount += 1; + removedDataSize = removedDataSize.plus(root.rawSize); // Use correct field name + + // Mark the Root entity as removed instead of deleting + root.removed = true; + root.updatedAt = event.block.timestamp; + root.blockNumber = event.block.number; + root.save(); + + // Update SumTree + const sumTree = new SumTree(); + sumTree.sumTreeRemove( + setId.toI32(), + proofSet.nextPieceId.toI32(), + rootId.toI32(), + root.rawSize.div(BigInt.fromI32(LeafSize)), + event.block.number + ); + } else { + log.warning( + "handlePiecesRemoved: Root {} for Set {} not found. Cannot remove.", + [rootId.toString(), setId.toString()] + ); + } + } + + // Update DataSet stats + proofSet.totalRoots = proofSet.totalRoots.minus( + BigInt.fromI32(removedRootCount) + ); // Use correct field name + proofSet.totalDataSize = proofSet.totalDataSize.minus(removedDataSize); + proofSet.leafCount = proofSet.leafCount.minus( + removedDataSize.div(BigInt.fromI32(LeafSize)) + ); + + // Ensure stats don't go negative + if (proofSet.totalRoots.lt(BigInt.fromI32(0))) { + // Use correct field name + log.warning( + "handlePiecesRemoved: DataSet {} rootCount went negative. Setting to 0.", + [setId.toString()] + ); + proofSet.totalRoots = BigInt.fromI32(0); // Use correct field name + } + if (proofSet.totalDataSize.lt(BigInt.fromI32(0))) { + log.warning( + "handlePiecesRemoved: DataSet {} totalDataSize went negative. Setting to 0.", + [setId.toString()] + ); + proofSet.totalDataSize = BigInt.fromI32(0); + } + if (proofSet.leafCount.lt(BigInt.fromI32(0))) { + log.warning( + "handlePiecesRemoved: DataSet {} leafCount went negative. Setting to 0.", + [setId.toString()] + ); + proofSet.leafCount = BigInt.fromI32(0); + } + proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); + proofSet.updatedAt = event.block.timestamp; + proofSet.blockNumber = event.block.number; + proofSet.save(); + + // Update Provider stats + const provider = Provider.load(proofSet.owner); + if (provider) { + provider.totalDataSize = provider.totalDataSize.minus(removedDataSize); + // Ensure provider totalDataSize doesn't go negative + if (provider.totalDataSize.lt(BigInt.fromI32(0))) { + log.warning( + "handlePiecesRemoved: Provider {} totalDataSize went negative. Setting to 0.", + [proofSet.owner.toHex()] + ); + provider.totalDataSize = BigInt.fromI32(0); + } + provider.totalRoots = provider.totalRoots.minus( + BigInt.fromI32(removedRootCount) + ); + // Ensure provider totalRoots doesn't go negative + if (provider.totalRoots.lt(BigInt.fromI32(0))) { + log.warning( + "handlePiecesRemoved: Provider {} totalRoots went negative. Setting to 0.", + [proofSet.owner.toHex()] + ); + provider.totalRoots = BigInt.fromI32(0); + } + provider.updatedAt = event.block.timestamp; + provider.blockNumber = event.block.number; + provider.save(); + } else { + log.warning("handlePiecesRemoved: Provider {} for DataSet {} not found", [ + proofSet.owner.toHex(), + setId.toString(), + ]); + } + + // update Service stats + const service = Service.load(proofSet.listener); + if (service) { + service.totalRoots = service.totalRoots.minus( + BigInt.fromI32(removedRootCount) + ); + // ensure totalRoots doesn't go negative + if (service.totalRoots.lt(BigInt.fromI32(0))) { + log.warning( + "handlePiecesRemoved: Service {} totalRoots went negative. Setting to 0.", + [proofSet.listener.toHex()] + ); + service.totalRoots = BigInt.fromI32(0); + } + service.totalDataSize = service.totalDataSize.minus(removedDataSize); + // ensure totalDataSize doesn't go negative + if (service.totalDataSize.lt(BigInt.fromI32(0))) { + log.warning( + "handlePiecesRemoved: Service {} totalDataSize went negative. Setting to 0.", + [proofSet.listener.toHex()] + ); + service.totalDataSize = BigInt.fromI32(0); + } + service.updatedAt = event.block.number; + service.save(); + } + + // Update network metrics + saveNetworkMetrics( + ["totalActiveRoots", "totalDataSize"], + [BigInt.fromI32(removedRootCount), removedDataSize], + ["subtract", "subtract"] + ); + + // Update provider and proof set metrics + const weekId = event.block.timestamp.toI32() / 604800; + const monthId = event.block.timestamp.toI32() / 2592000; + const providerId = proofSet.owner; + const weeklyProviderId = Bytes.fromI32(weekId).concat(providerId); + const monthlyProviderId = Bytes.fromI32(monthId).concat(providerId); + const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); + const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); + saveProviderMetrics( + "WeeklyProviderActivity", + weeklyProviderId, + providerId, + ["totalRootsRemoved", "totalDataSizeRemoved"], + [BigInt.fromI32(removedRootCount), removedDataSize], + ["add", "add"] + ); + saveProviderMetrics( + "MonthlyProviderActivity", + monthlyProviderId, + providerId, + ["totalRootsRemoved", "totalDataSizeRemoved"], + [BigInt.fromI32(removedRootCount), removedDataSize], + ["add", "add"] + ); + saveProofSetMetrics( + "WeeklyProofSetActivity", + weeklyProofSetId, + setId, + ["totalRootsRemoved", "totalDataSizeRemoved"], + [BigInt.fromI32(removedRootCount), removedDataSize], + ["add", "add"] + ); + saveProofSetMetrics( + "MonthlyProofSetActivity", + monthlyProofSetId, + setId, + ["totalRootsRemoved", "totalDataSizeRemoved"], + [BigInt.fromI32(removedRootCount), removedDataSize], + ["add", "add"] + ); +} + +// Helper function to read Uint256 from Bytes at a specific offset +function readUint256(data: Bytes, offset: i32): BigInt { + if (offset < 0 || data.length < offset + 32) { + log.error( + "readUint256: Invalid offset {} or data length {} for reading Uint256", + [offset.toString(), data.length.toString()] + ); + return BigInt.zero(); + } + // Slice 32 bytes and convert to BigInt (assuming big-endian) + const slicedBytes = Bytes.fromUint8Array(data.slice(offset, offset + 32)); + // Ensure bytes are reversed for correct BigInt conversion if needed (depends on source endianness) + // AssemblyScript's BigInt.fromUnsignedBytes assumes little-endian by default, reverse for big-endian + const reversedBytesArray = slicedBytes.reverse(); // Returns Uint8Array + const reversedBytes = Bytes.fromUint8Array(reversedBytesArray); // Create Bytes object + return BigInt.fromUnsignedBytes(reversedBytes); +} + +// Helper function to read dynamic Bytes from ABI-encoded data +function readBytes(data: Bytes, offset: i32): Bytes { + // First, read the offset to the actual bytes data (uint256) + const bytesTupleOffset = readUint256(data, offset).toI32(); + + // Check if the bytes offset is valid + if (bytesTupleOffset < 0 || data.length < offset + bytesTupleOffset + 32) { + log.error( + "readBytes: Invalid offset {} or data length {} for reading bytes length", + [bytesTupleOffset.toString(), data.length.toString()] + ); + return Bytes.empty(); + } + + const bytesOffset = readUint256(data, offset + bytesTupleOffset).toI32(); + const bytesAbsOffset = offset + bytesTupleOffset + bytesOffset; + // Read the length of the bytes (uint256) + const bytesLength = readUint256(data, bytesAbsOffset).toI32(); + + // Check if the length is valid + if (bytesLength < 0 || data.length < bytesAbsOffset + 32 + bytesLength) { + log.error( + "readBytes: Invalid length {} or data length {} for reading bytes data", + [bytesLength.toString(), data.length.toString()] + ); + return Bytes.empty(); + } + + // Slice the actual bytes + return Bytes.fromUint8Array( + data.slice(bytesAbsOffset + 32, bytesAbsOffset + 32 + bytesLength) + ); +} diff --git a/apps/subgraph/src/sumTree.ts b/apps/subgraph/src/sumTree.ts new file mode 100644 index 00000000..29084324 --- /dev/null +++ b/apps/subgraph/src/sumTree.ts @@ -0,0 +1,200 @@ +import { BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { SumTreeCount } from "../generated/schema"; + +// Define a class for the structure instead of a type alias +class PieceIdAndOffset { + rootId: BigInt; + offset: BigInt; +} + +export class SumTree { + private getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { + return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); + } + + // Helper: Get sumTreeCounts[setId][index], default 0 + private getSum(setId: i32, index: i32, blockNumber: BigInt): BigInt { + const rootEntityId = this.getRootEntityId( + BigInt.fromI32(setId as i32), + BigInt.fromI32(index as i32) + ); + const sumTreeCount = SumTreeCount.load(rootEntityId); + if (!sumTreeCount) return BigInt.fromI32(0); + if (sumTreeCount.lastDecEpoch.equals(blockNumber)) { + return sumTreeCount.lastCount; + } + return sumTreeCount.count; + } + + // Helper: Set sumTreeCounts[setId][index] = value + private setSum(setId: i32, index: i32, value: BigInt): void { + const rootEntityId = this.getRootEntityId( + BigInt.fromI32(setId), + BigInt.fromI32(index) + ); + const sumTreeCount = new SumTreeCount(rootEntityId); + sumTreeCount.setId = BigInt.fromI32(setId as i32); + sumTreeCount.rootId = BigInt.fromI32(index as i32); + sumTreeCount.count = value; + sumTreeCount.lastCount = BigInt.fromI32(0); + sumTreeCount.lastDecEpoch = BigInt.fromI32(0); + sumTreeCount.save(); + } + + // Helper: Decrement sumTreeCounts[setId][index] by delta + private decSum( + setId: i32, + index: i32, + delta: BigInt, + blockNumber: BigInt + ): void { + const rootEntityId = this.getRootEntityId( + BigInt.fromI32(setId), + BigInt.fromI32(index) + ); + const sumTreeCount = SumTreeCount.load(rootEntityId); + if (!sumTreeCount) return; + const prev = sumTreeCount.count; + sumTreeCount.lastCount = prev; + sumTreeCount.count = prev.minus(delta); + sumTreeCount.lastDecEpoch = blockNumber; + sumTreeCount.save(); + } + + // Helper: heightFromIndex (number of trailing zeros in index+1) + private heightFromIndex(index: i32): i32 { + let x = index + 1; + let tz = 0; + while ((x & 1) === 0) { + tz++; + x >>= 1; + } + return tz; + } + + // Helper: clz (count leading zeros) for 32-bit numbers + private clz(x: i32): i32 { + if (x === 0) return 32; + let n = 32; + let y = (x as u32) >> 16; + if (y !== 0) { + n -= 16; + x = y; + } + y = (x as u32) >> 8; + if (y !== 0) { + n -= 8; + x = y; + } + y = (x as u32) >> 4; + if (y !== 0) { + n -= 4; + x = y; + } + y = (x as u32) >> 2; + if (y !== 0) { + n -= 2; + x = y; + } + y = (x as u32) >> 1; + if (y !== 0) { + return n - 2; + } + return n - (x as i32); + } + + // sumTreeAdd + sumTreeAdd(setId: i32, count: BigInt, rootId: i32): void { + let index = rootId; + let h = this.heightFromIndex(index); + let sum = count; + for (let i = 0; i < h; i++) { + let j = index - (1 << i); + sum = sum.plus(this.getSum(setId, j, BigInt.fromI32(1))); // 0 is default value of lastDecEpoch so using 1 + } + this.setSum(setId, rootId, sum); + } + + // sumTreeRemove + sumTreeRemove( + setId: i32, + nextRoot: i32, + index: i32, + delta: BigInt, + blockNumber: BigInt + ): void { + const top = 32 - this.clz(nextRoot); + let h = this.heightFromIndex(index); + while (h <= top && index < nextRoot) { + this.decSum(setId, index, delta, blockNumber); + index += 1 << h; + h = this.heightFromIndex(index); + } + } + + // findOneRootId + findOneRootId( + setId: i32, + nextRoot: i32, + leafIndex: BigInt, + top: i32, + blockNumber: BigInt + ): PieceIdAndOffset { + let searchPtr = (1 << top) - 1; + let acc: BigInt = BigInt.fromI32(0); + let candidate: BigInt = BigInt.fromI32(0); + for (let h = top; h > 0; h--) { + if (searchPtr >= nextRoot) { + searchPtr -= 1 << (h - 1); + continue; + } + const sum = this.getSum(setId, searchPtr, blockNumber); + candidate = acc.plus(sum); + if (candidate.le(leafIndex)) { + acc = acc.plus(sum); + searchPtr += 1 << (h - 1); + } else { + searchPtr -= 1 << (h - 1); + } + } + candidate = acc.plus(this.getSum(setId, searchPtr, blockNumber)); + if (candidate.le(leafIndex)) { + return { + rootId: BigInt.fromI32(searchPtr + 1), + offset: leafIndex.minus(candidate), + }; + } + return { + rootId: BigInt.fromI32(searchPtr), + offset: leafIndex.minus(acc), + }; + } + + // findPieceIds (batched) + findPieceIds( + setId: i32, + nextPieceId: i32, + leafIndexes: BigInt[], + blockNumber: BigInt + ): PieceIdAndOffset[] { + const top = 32 - this.clz(nextPieceId); + + const results: PieceIdAndOffset[] = []; + for (let i = 0; i < leafIndexes.length; i++) { + const idx = leafIndexes[i]; + + const result = this.findOneRootId( + setId, + nextPieceId, + idx, + top, + blockNumber + ); + results.push(result); + } + + return results; + } +} + +export default SumTree; diff --git a/apps/subgraph/src/types.ts b/apps/subgraph/src/types.ts new file mode 100644 index 00000000..935ffdea --- /dev/null +++ b/apps/subgraph/src/types.ts @@ -0,0 +1,6 @@ +export class DataSetStatus { + static readonly EMPTY: string = "EMPTY"; + static readonly READY: string = "READY"; + static readonly PROVING: string = "PROVING"; + static readonly DELETED: string = "DELETED"; +} diff --git a/apps/subgraph/subgraph.yaml b/apps/subgraph/subgraph.yaml new file mode 100644 index 00000000..0e440d0a --- /dev/null +++ b/apps/subgraph/subgraph.yaml @@ -0,0 +1,84 @@ +specVersion: 1.3.0 +indexerHints: + prune: auto +schema: + file: ./schema.graphql +dataSources: + - kind: ethereum + name: PDPVerifier + network: filecoin + source: + abi: PDPVerifier + address: "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F" + startBlock: 5441432 + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + entities: + - DataSet + - Provider + - Root + - SumTreeCount + - Service + - ServiceProviderLink + - EventLog + - Transaction + - Proof + - ProofFee + - FaultRecord + abis: + - name: PDPVerifier + file: ./abis/PDPVerifier.json + eventHandlers: + - event: DataSetCreated(indexed uint256,indexed address) + handler: handleDataSetCreated + - event: StorageProviderChanged(indexed uint256,indexed address,indexed address) + handler: handleStorageProviderChanged + - event: DataSetDeleted(indexed uint256,uint256) + handler: handleDataSetDeleted + - event: PiecesAdded(indexed uint256,uint256[],(bytes)[]) + handler: handlePiecesAdded + - event: PiecesRemoved(indexed uint256,uint256[]) + handler: handlePiecesRemoved + - event: ProofFeePaid(indexed uint256,uint256) + handler: handleProofFeePaid + - event: DataSetEmpty(indexed uint256) + handler: handleDataSetEmpty + - event: PossessionProven(indexed uint256,(uint256,uint256)[]) + handler: handlePossessionProven + - event: NextProvingPeriod(indexed uint256,uint256,uint256) + handler: handleNextProvingPeriod + file: ./src/pdp-verifier.ts + - kind: ethereum + name: FilecoinWarmStorageService + network: filecoin + source: + abi: FilecoinWarmStorageService + address: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f" + startBlock: 5459617 + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + entities: + - DataSet + - Root + abis: + - name: FilecoinWarmStorageService + file: ./abis/FilecoinWarmStorageService.json + eventHandlers: + - event: DataSetCreated(indexed uint256,indexed + uint256,uint256,uint256,uint256,address,address,address,string[],string[]) + handler: handleFwssDataSetCreated + - event: PieceAdded(indexed uint256,indexed uint256,(bytes),string[],string[]) + handler: handleFwssPieceAdded + - event: ServiceTerminated(indexed address,indexed + uint256,uint256,uint256,uint256) + handler: handleFwssServiceTerminated + - event: PDPPaymentTerminated(indexed uint256,uint256,uint256) + handler: handleFwssPdpPaymentTerminated + - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed + address) + handler: handleFwssDataSetServiceProviderChanged + file: ./src/fwss.ts diff --git a/apps/subgraph/tests/dataset-status.test.ts b/apps/subgraph/tests/dataset-status.test.ts new file mode 100644 index 00000000..8053f545 --- /dev/null +++ b/apps/subgraph/tests/dataset-status.test.ts @@ -0,0 +1,548 @@ +import { + assert, + describe, + test, + clearStore, + afterEach, +} from "matchstick-as/assembly/index"; +import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + handleDataSetCreated, + handlePiecesAdded, + handleNextProvingPeriod, + handleDataSetDeleted, + handleDataSetEmpty, +} from "../src/pdp-verifier"; +import { + createDataSetCreatedEvent, + createRootsAddedEvent, + createNextProvingPeriodEvent, + createDataSetDeletedEvent, + createDataSetEmptyEvent, + generateTxHash, +} from "./pdp-verifier-utils"; + +const SET_ID = BigInt.fromI32(1); +const ROOT_ID_1 = BigInt.fromI32(101); +const SENDER_ADDRESS = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" +); +const CONTRACT_ADDRESS = Address.fromString( + "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" +); +const PROOF_SET_ID_BYTES = Bytes.fromBigInt(SET_ID); + +describe("DataSetStatus Lifecycle Tests", () => { + afterEach(() => { + clearStore(); + }); + + test("handleDataSetCreated sets status to EMPTY", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(1), + BigInt.fromI32(0) + ); + + handleDataSetCreated(mockDataSetCreatedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); + assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + }); + + test("handlePiecesAdded transitions status from EMPTY to READY", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(10), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent.block.number = BigInt.fromI32(150); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = generateTxHash(11); + + handlePiecesAdded(rootsAddedEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); + }); + + test("handleNextProvingPeriod transitions status from READY to PROVING", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(20), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent.block.number = BigInt.fromI32(150); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = generateTxHash(21); + handlePiecesAdded(rootsAddedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + + let nextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(200), + BigInt.fromI32(32), + CONTRACT_ADDRESS, + BigInt.fromI32(200), + BigInt.fromI32(1678886600), + generateTxHash(22), + BigInt.fromI32(0) + ); + + handleNextProvingPeriod(nextProvingPeriodEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); + assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); + assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "200"); + }); + + test("handleDataSetDeleted transitions status to DELETED", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(30), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent.block.number = BigInt.fromI32(150); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = generateTxHash(31); + handlePiecesAdded(rootsAddedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + + let dataSetDeletedEvent = createDataSetDeletedEvent( + SET_ID, + BigInt.fromI32(32), + CONTRACT_ADDRESS, + BigInt.fromI32(200), + BigInt.fromI32(1678886700), + generateTxHash(32), + BigInt.fromI32(0) + ); + + handleDataSetDeleted(dataSetDeletedEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "DELETED"); + assert.fieldEquals("DataSet", dataSetId, "isActive", "false"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); + assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + }); + + test("handleDataSetEmpty transitions status to EMPTY", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(40), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent.block.number = BigInt.fromI32(150); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = generateTxHash(41); + handlePiecesAdded(rootsAddedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + + let dataSetEmptyEvent = createDataSetEmptyEvent( + SET_ID, + CONTRACT_ADDRESS, + BigInt.fromI32(200), + BigInt.fromI32(1678886700), + generateTxHash(42), + BigInt.fromI32(0) + ); + + handleDataSetEmpty(dataSetEmptyEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); + assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); + }); + + test("handleDataSetDeleted from PROVING status transitions to DELETED", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(50), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent.block.number = BigInt.fromI32(150); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = generateTxHash(51); + handlePiecesAdded(rootsAddedEvent); + + let nextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(200), + BigInt.fromI32(32), + CONTRACT_ADDRESS, + BigInt.fromI32(200), + BigInt.fromI32(1678886600), + generateTxHash(52), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(nextProvingPeriodEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); + + let dataSetDeletedEvent = createDataSetDeletedEvent( + SET_ID, + BigInt.fromI32(32), + CONTRACT_ADDRESS, + BigInt.fromI32(250), + BigInt.fromI32(1678886800), + generateTxHash(53), + BigInt.fromI32(0) + ); + + handleDataSetDeleted(dataSetDeletedEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "DELETED"); + assert.fieldEquals("DataSet", dataSetId, "isActive", "false"); + }); + + test("handleDataSetEmpty from PROVING status transitions to EMPTY", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(60), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent.block.number = BigInt.fromI32(150); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = generateTxHash(61); + handlePiecesAdded(rootsAddedEvent); + + let nextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(200), + BigInt.fromI32(32), + CONTRACT_ADDRESS, + BigInt.fromI32(200), + BigInt.fromI32(1678886600), + generateTxHash(62), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(nextProvingPeriodEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); + + let dataSetEmptyEvent = createDataSetEmptyEvent( + SET_ID, + CONTRACT_ADDRESS, + BigInt.fromI32(250), + BigInt.fromI32(1678886800), + generateTxHash(63), + BigInt.fromI32(0) + ); + + handleDataSetEmpty(dataSetEmptyEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); + assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + }); + + test("Status remains EMPTY when no pieces are added", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(70), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); + }); + + test("Multiple pieces added keeps status as READY", () => { + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(80), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let pieceIds1 = [ROOT_ID_1]; + let rootsAddedEvent1 = createRootsAddedEvent( + SET_ID, + pieceIds1, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent1.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent1.block.number = BigInt.fromI32(150); + rootsAddedEvent1.logIndex = BigInt.fromI32(1); + rootsAddedEvent1.transaction.hash = generateTxHash(81); + handlePiecesAdded(rootsAddedEvent1); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + + let pieceIds2 = [BigInt.fromI32(102)]; + let rootsAddedEvent2 = createRootsAddedEvent( + SET_ID, + pieceIds2, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent2.block.timestamp = BigInt.fromI32(1678886600); + rootsAddedEvent2.block.number = BigInt.fromI32(160); + rootsAddedEvent2.logIndex = BigInt.fromI32(1); + rootsAddedEvent2.transaction.hash = generateTxHash(82); + handlePiecesAdded(rootsAddedEvent2); + + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + }); + + test("Lifecycle: Add roots → Empty → Add roots again (with event sequence)", () => { + // Step 1: Create dataset (status = EMPTY) + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(90), + BigInt.fromI32(0) + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); + assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); + assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "0"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); + + // Step 2: Add roots (status = EMPTY → READY) + let pieceIds1 = [ROOT_ID_1]; + let rootsAddedEvent1 = createRootsAddedEvent( + SET_ID, + pieceIds1, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent1.block.timestamp = BigInt.fromI32(1678886500); + rootsAddedEvent1.block.number = BigInt.fromI32(150); + rootsAddedEvent1.logIndex = BigInt.fromI32(1); + rootsAddedEvent1.transaction.hash = generateTxHash(91); + handlePiecesAdded(rootsAddedEvent1); + + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "1"); + assert.fieldEquals("DataSet", dataSetId, "leafCount", "327715"); + + // Step 3: NextProvingPeriod (status = READY → PROVING) + let nextProvingPeriodEvent1 = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(1), + BigInt.fromI32(327715), + CONTRACT_ADDRESS + ); + nextProvingPeriodEvent1.block.timestamp = BigInt.fromI32(1678886600); + nextProvingPeriodEvent1.block.number = BigInt.fromI32(200); + nextProvingPeriodEvent1.logIndex = BigInt.fromI32(1); + nextProvingPeriodEvent1.transaction.hash = generateTxHash(92); + handleNextProvingPeriod(nextProvingPeriodEvent1); + + assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); + assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "200"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "440"); // 200 + 240 + assert.fieldEquals("DataSet", dataSetId, "currentDeadlineCount", "1"); + + // Step 4: Dataset becomes empty (PiecesRemoved → DataSetEmpty → NextProvingPeriod in same tx) + // Simulate the event sequence from contract's nextProvingPeriod function + + // Event 1: DataSetEmpty (emitted by contract) + let dataSetEmptyEvent = createDataSetEmptyEvent(SET_ID, CONTRACT_ADDRESS); + dataSetEmptyEvent.block.timestamp = BigInt.fromI32(1678886700); + dataSetEmptyEvent.block.number = BigInt.fromI32(250); + dataSetEmptyEvent.logIndex = BigInt.fromI32(1); + dataSetEmptyEvent.transaction.hash = generateTxHash(93); + handleDataSetEmpty(dataSetEmptyEvent); + + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); + assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); + assert.fieldEquals("DataSet", dataSetId, "nextChallengeEpoch", "0"); + assert.fieldEquals("DataSet", dataSetId, "lastProvenEpoch", "0"); + + // Event 2: NextProvingPeriod (same transaction, should handle empty dataset) + let nextProvingPeriodEvent2 = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(2), + BigInt.fromI32(0), + CONTRACT_ADDRESS + ); + nextProvingPeriodEvent2.block.timestamp = BigInt.fromI32(1678886700); + nextProvingPeriodEvent2.block.number = BigInt.fromI32(250); + nextProvingPeriodEvent2.logIndex = BigInt.fromI32(2); + nextProvingPeriodEvent2.transaction.hash = generateTxHash(93); // Same tx hash + handleNextProvingPeriod(nextProvingPeriodEvent2); + + // Verify dataset remains EMPTY and proving fields are reset + assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); + assert.fieldEquals("DataSet", dataSetId, "nextChallengeEpoch", "0"); + assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "0"); + assert.fieldEquals("DataSet", dataSetId, "maxProvingPeriod", "0"); + assert.fieldEquals("DataSet", dataSetId, "challengeWindowSize", "0"); + assert.fieldEquals("DataSet", dataSetId, "currentDeadlineCount", "0"); + assert.fieldEquals("DataSet", dataSetId, "totalFaultedPeriods", "1"); + + // Step 5: Add roots again (status = EMPTY → READY) + let pieceIds2 = [BigInt.fromI32(201)]; + let rootsAddedEvent2 = createRootsAddedEvent( + SET_ID, + pieceIds2, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + rootsAddedEvent2.block.timestamp = BigInt.fromI32(1678886800); + rootsAddedEvent2.block.number = BigInt.fromI32(300); + rootsAddedEvent2.logIndex = BigInt.fromI32(1); + rootsAddedEvent2.transaction.hash = generateTxHash(94); + handlePiecesAdded(rootsAddedEvent2); + + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "1"); + assert.fieldEquals("DataSet", dataSetId, "leafCount", "327715"); + + // Step 6: NextProvingPeriod again (status = READY → PROVING with new firstDeadline) + let nextProvingPeriodEvent3 = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(1), // Challenge epoch resets + BigInt.fromI32(327715), + CONTRACT_ADDRESS + ); + nextProvingPeriodEvent3.block.timestamp = BigInt.fromI32(1678886900); + nextProvingPeriodEvent3.block.number = BigInt.fromI32(350); + nextProvingPeriodEvent3.logIndex = BigInt.fromI32(1); + nextProvingPeriodEvent3.transaction.hash = generateTxHash(95); + handleNextProvingPeriod(nextProvingPeriodEvent3); + + assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); + assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "350"); // new firstDeadline, not 200 + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "590"); // 350 + 240 + assert.fieldEquals("DataSet", dataSetId, "currentDeadlineCount", "1"); // Resets to 1 + assert.fieldEquals("DataSet", dataSetId, "maxProvingPeriod", "240"); + assert.fieldEquals("DataSet", dataSetId, "challengeWindowSize", "20"); + }); +}); diff --git a/apps/subgraph/tests/fault-calculation.test.ts b/apps/subgraph/tests/fault-calculation.test.ts new file mode 100644 index 00000000..14c3cc6a --- /dev/null +++ b/apps/subgraph/tests/fault-calculation.test.ts @@ -0,0 +1,962 @@ +import { + assert, + describe, + test, + clearStore, + beforeEach, + afterEach, +} from "matchstick-as/assembly/index"; +import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + handleDataSetCreated, + handleNextProvingPeriod, + handlePiecesAdded, + handlePossessionProven, +} from "../src/pdp-verifier"; +import { + createDataSetCreatedEvent, + createNextProvingPeriodEvent, + createPossessionProvenEvent, + createRootsAddedEvent, + generateTxHash, +} from "./pdp-verifier-utils"; + +const SET_ID = BigInt.fromI32(1); +const ROOT_ID_1 = BigInt.fromI32(101); +const PROVIDER_ADDRESS = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" +); +const CONTRACT_ADDRESS = Address.fromString( + "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" +); +const LISTENER_ADDRESS = Address.fromString( + "0x0000000000000000000000000000000000000001" +); +const MAX_PROVING_PERIOD = BigInt.fromI32(240); +const CHALLENGE_WINDOW_SIZE = BigInt.fromI32(20); +const SENDER_ADDRESS = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" +); + +function getProofSetId(): string { + return Bytes.fromBigInt(SET_ID).toHex(); +} + +function getProviderId(): string { + return PROVIDER_ADDRESS.toHex(); +} + +function addRootToDataSet(setId: BigInt, rootId: BigInt): void { + const rootsAddedEvent = createRootsAddedEvent( + setId, + [rootId], + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + + // Set block/tx details on the mock event if needed by handler + rootsAddedEvent.block.timestamp = BigInt.fromI32(100); // Example timestamp + rootsAddedEvent.block.number = BigInt.fromI32(50); // Example block number + rootsAddedEvent.logIndex = BigInt.fromI32(1); // Example log index + rootsAddedEvent.transaction.hash = Bytes.fromHexString("0x" + "c".repeat(64)); + + handlePiecesAdded(rootsAddedEvent); +} + +describe("Fault Calculation Tests", () => { + beforeEach(() => { + clearStore(); + }); + + afterEach(() => { + clearStore(); + }); + + test("Test 1: DataSet creation initializes with zero values", () => { + const blockNumber = BigInt.fromI32(100); + const timestamp = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + blockNumber, + timestamp, + generateTxHash(100), + BigInt.fromI32(0) + ); + + handleDataSetCreated(dataSetCreatedEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + assert.entityCount("DataSet", 1); + assert.entityCount("Provider", 1); + + assert.fieldEquals("DataSet", proofSetId, "setId", SET_ID.toString()); + assert.fieldEquals("DataSet", proofSetId, "nextDeadline", "0"); + assert.fieldEquals("DataSet", proofSetId, "firstDeadline", "0"); + assert.fieldEquals("DataSet", proofSetId, "maxProvingPeriod", "0"); + assert.fieldEquals("DataSet", proofSetId, "challengeWindowSize", "0"); + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "0"); + assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); + assert.fieldEquals("DataSet", proofSetId, "totalFaultedPeriods", "0"); + + assert.fieldEquals("Provider", providerId, "totalFaultedPeriods", "0"); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "0"); + }); + + test("Test 2: First nextProvingPeriod call sets initial deadline", () => { + const createBlockNumber = BigInt.fromI32(100); + const createTimestamp = BigInt.fromI32(1000); + const firstProvingBlockNumber = BigInt.fromI32(150); + const firstProvingTimestamp = BigInt.fromI32(1500); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + createTimestamp, + generateTxHash(200), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const nextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + firstProvingTimestamp, + generateTxHash(201), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(nextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + const expectedNextDeadline = + firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + + assert.fieldEquals( + "DataSet", + proofSetId, + "firstDeadline", + firstProvingBlockNumber.toString() + ); + assert.fieldEquals( + "DataSet", + proofSetId, + "maxProvingPeriod", + MAX_PROVING_PERIOD.toString() + ); + assert.fieldEquals( + "DataSet", + proofSetId, + "challengeWindowSize", + CHALLENGE_WINDOW_SIZE.toString() + ); + assert.fieldEquals( + "DataSet", + proofSetId, + "nextDeadline", + expectedNextDeadline.toString() + ); + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "1"); + assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); + assert.fieldEquals("DataSet", proofSetId, "totalFaultedPeriods", "0"); + assert.fieldEquals( + "DataSet", + proofSetId, + "nextChallengeEpoch", + challengeEpoch.toString() + ); + assert.fieldEquals( + "DataSet", + proofSetId, + "challengeRange", + leafCount.toString() + ); + + assert.fieldEquals("Provider", providerId, "totalFaultedPeriods", "0"); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "1"); + + const provingWindowId = Bytes.fromUTF8(SET_ID.toString() + "-1").toHex(); + assert.entityCount("ProvingWindow", 1); + assert.fieldEquals("ProvingWindow", provingWindowId, "deadlineCount", "1"); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "deadline", + expectedNextDeadline.toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "windowStart", + expectedNextDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "windowEnd", + expectedNextDeadline.toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "proofSubmitted", + "false" + ); + assert.fieldEquals("ProvingWindow", provingWindowId, "isValid", "false"); + }); + + test("Test 3: Second nextProvingPeriod without proof submission - 1 faulted period", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const secondProvingBlockNumber = BigInt.fromI32(400); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(300), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(301), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(2000), + generateTxHash(302), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + const periodsSkipped = secondProvingBlockNumber + .minus(firstDeadline.plus(BigInt.fromI32(1))) + .div(MAX_PROVING_PERIOD); + const expectedNextDeadline = firstDeadline.plus( + MAX_PROVING_PERIOD.times(periodsSkipped.plus(BigInt.fromI32(1))) + ); + const expectedFaultedPeriods = periodsSkipped.plus(BigInt.fromI32(1)); + + assert.fieldEquals( + "DataSet", + proofSetId, + "nextDeadline", + expectedNextDeadline.toString() + ); + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); + assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); + assert.fieldEquals( + "DataSet", + proofSetId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + + assert.fieldEquals( + "Provider", + providerId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); + }); + + test("Test 4: Proof submission marks period as proven", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const proofBlockNumber = BigInt.fromI32(370); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(400), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(401), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const possessionProvenEvent = createPossessionProvenEvent( + SET_ID, + [ROOT_ID_1], + [BigInt.fromI32(100)], + CONTRACT_ADDRESS, + proofBlockNumber, + BigInt.fromI32(1800), + generateTxHash(402), + BigInt.fromI32(0) + ); + handlePossessionProven(possessionProvenEvent); + + const proofSetId = getProofSetId(); + const provingWindowId = Bytes.fromUTF8(SET_ID.toString() + "-1").toHex(); + + assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "true"); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "proofSubmitted", + "true" + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "proofBlockNumber", + proofBlockNumber.toString() + ); + assert.fieldEquals("ProvingWindow", provingWindowId, "isValid", "true"); + }); + + test("Test 5: Third nextProvingPeriod with proof - 0 faulted periods", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const proofBlockNumber = BigInt.fromI32(370); + const secondProvingBlockNumber = BigInt.fromI32(400); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(500), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(501), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const possessionProvenEvent = createPossessionProvenEvent( + SET_ID, + [ROOT_ID_1], + [BigInt.fromI32(100)], + CONTRACT_ADDRESS, + proofBlockNumber, + BigInt.fromI32(1800), + generateTxHash(502), + BigInt.fromI32(0) + ); + handlePossessionProven(possessionProvenEvent); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(2000), + generateTxHash(503), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + const periodsSkipped = secondProvingBlockNumber + .minus(firstDeadline.plus(BigInt.fromI32(1))) + .div(MAX_PROVING_PERIOD); + + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); + assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); + assert.fieldEquals( + "DataSet", + proofSetId, + "totalFaultedPeriods", + periodsSkipped.toString() + ); + + assert.fieldEquals( + "Provider", + providerId, + "totalFaultedPeriods", + periodsSkipped.toString() + ); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); + }); + + test("Test 6: Multiple periods skipped - calculates correct faulted periods", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const secondProvingBlockNumber = BigInt.fromI32(900); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(600), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(601), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(3000), + generateTxHash(602), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + const periodsSkipped = secondProvingBlockNumber + .minus(firstDeadline.plus(BigInt.fromI32(1))) + .div(MAX_PROVING_PERIOD); + const expectedFaultedPeriods = periodsSkipped.plus(BigInt.fromI32(1)); + const expectedDeadlineCount = periodsSkipped.plus(BigInt.fromI32(2)); + const expectedNextDeadline = firstDeadline.plus( + MAX_PROVING_PERIOD.times(periodsSkipped.plus(BigInt.fromI32(1))) + ); + + assert.fieldEquals( + "DataSet", + proofSetId, + "nextDeadline", + expectedNextDeadline.toString() + ); + assert.fieldEquals( + "DataSet", + proofSetId, + "currentDeadlineCount", + expectedDeadlineCount.toString() + ); + assert.fieldEquals( + "DataSet", + proofSetId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + + assert.fieldEquals( + "Provider", + providerId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + assert.fieldEquals( + "Provider", + providerId, + "totalProvingPeriods", + expectedDeadlineCount.toString() + ); + }); + + test("Test 7: nextProvingPeriod called before deadline - no periods skipped but pervious period faulted", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const secondProvingBlockNumber = BigInt.fromI32(380); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(700), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(701), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(2000), + generateTxHash(702), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + const expectedFaultedPeriods = BigInt.fromI32(1); + const expectedNextDeadline = firstDeadline.plus(MAX_PROVING_PERIOD); + + assert.fieldEquals( + "DataSet", + proofSetId, + "nextDeadline", + expectedNextDeadline.toString() + ); + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); + assert.fieldEquals( + "DataSet", + proofSetId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + + assert.fieldEquals( + "Provider", + providerId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); + }); + + test("Test 8: nextProvingPeriod called exactly at deadline - 1 faulted period", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const secondProvingBlockNumber = BigInt.fromI32(390); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(800), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(801), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(2000), + generateTxHash(802), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + const expectedFaultedPeriods = BigInt.fromI32(1); + const expectedNextDeadline = firstDeadline.plus(MAX_PROVING_PERIOD); + + assert.fieldEquals( + "DataSet", + proofSetId, + "nextDeadline", + expectedNextDeadline.toString() + ); + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); + assert.fieldEquals( + "DataSet", + proofSetId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + + assert.fieldEquals( + "Provider", + providerId, + "totalFaultedPeriods", + expectedFaultedPeriods.toString() + ); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); + }); + + test("Test 9: Verify ProvingWindow entities created for skipped periods", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const secondProvingBlockNumber = BigInt.fromI32(900); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(900), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(901), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(3000), + generateTxHash(902), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + + const periodsSkipped = secondProvingBlockNumber + .minus(firstDeadline.plus(BigInt.fromI32(1))) + .div(MAX_PROVING_PERIOD); + const expectedDeadlineCount = periodsSkipped.plus(BigInt.fromI32(2)); + + assert.fieldEquals( + "DataSet", + proofSetId, + "currentDeadlineCount", + expectedDeadlineCount.toString() + ); + + assert.entityCount("ProvingWindow", expectedDeadlineCount.toI32()); + + const provingWindow1Id = Bytes.fromUTF8(SET_ID.toString() + "-1").toHex(); + assert.fieldEquals("ProvingWindow", provingWindow1Id, "deadlineCount", "1"); + assert.fieldEquals( + "ProvingWindow", + provingWindow1Id, + "deadline", + firstDeadline.toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindow1Id, + "windowStart", + firstDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindow1Id, + "windowEnd", + firstDeadline.toString() + ); + + for (let i = 0; i < periodsSkipped.toI32(); i++) { + const deadlineCount = expectedDeadlineCount + .minus(periodsSkipped) + .plus(BigInt.fromI32(i)); + const expectedDeadline = firstProvingBlockNumber.plus( + deadlineCount.times(MAX_PROVING_PERIOD) + ); + const provingWindowId = Bytes.fromUTF8( + SET_ID.toString() + "-" + deadlineCount.toString() + ).toHex(); + + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "deadlineCount", + deadlineCount.toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "deadline", + expectedDeadline.toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "windowStart", + expectedDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "windowEnd", + expectedDeadline.toString() + ); + assert.fieldEquals( + "ProvingWindow", + provingWindowId, + "proofSubmitted", + "false" + ); + assert.fieldEquals("ProvingWindow", provingWindowId, "isValid", "false"); + } + + const finalDeadline = firstDeadline.plus( + MAX_PROVING_PERIOD.times(periodsSkipped.plus(BigInt.fromI32(1))) + ); + const finalProvingWindowId = Bytes.fromUTF8( + SET_ID.toString() + "-" + expectedDeadlineCount.toString() + ).toHex(); + assert.fieldEquals( + "ProvingWindow", + finalProvingWindowId, + "deadlineCount", + expectedDeadlineCount.toString() + ); + assert.fieldEquals( + "ProvingWindow", + finalProvingWindowId, + "deadline", + finalDeadline.toString() + ); + assert.fieldEquals( + "ProvingWindow", + finalProvingWindowId, + "windowStart", + finalDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() + ); + assert.fieldEquals( + "ProvingWindow", + finalProvingWindowId, + "windowEnd", + finalDeadline.toString() + ); + }); + + test("Test 10: Complex scenario - multiple proving periods with mixed proof submissions", () => { + const createBlockNumber = BigInt.fromI32(100); + const firstProvingBlockNumber = BigInt.fromI32(150); + const proofBlockNumber1 = BigInt.fromI32(370); + const secondProvingBlockNumber = BigInt.fromI32(400); + const thirdProvingBlockNumber = BigInt.fromI32(650); + const proofBlockNumber2 = BigInt.fromI32(870); + const fourthProvingBlockNumber = BigInt.fromI32(900); + const challengeEpoch = BigInt.fromI32(200); + const leafCount = BigInt.fromI32(1000); + + const dataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + CONTRACT_ADDRESS, + LISTENER_ADDRESS, + createBlockNumber, + BigInt.fromI32(1000), + generateTxHash(1000), + BigInt.fromI32(0) + ); + handleDataSetCreated(dataSetCreatedEvent); + addRootToDataSet(SET_ID, ROOT_ID_1); + + const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch, + leafCount, + CONTRACT_ADDRESS, + firstProvingBlockNumber, + BigInt.fromI32(1500), + generateTxHash(1001), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(firstNextProvingPeriodEvent); + + const possessionProvenEvent1 = createPossessionProvenEvent( + SET_ID, + [ROOT_ID_1], + [BigInt.fromI32(100)], + CONTRACT_ADDRESS, + proofBlockNumber1, + BigInt.fromI32(1800) + ); + handlePossessionProven(possessionProvenEvent1); + + const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(100)), + leafCount, + CONTRACT_ADDRESS, + secondProvingBlockNumber, + BigInt.fromI32(2000), + generateTxHash(1002), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(secondNextProvingPeriodEvent); + + const thirdNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(200)), + leafCount, + CONTRACT_ADDRESS, + thirdProvingBlockNumber, + BigInt.fromI32(2500), + generateTxHash(1003), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(thirdNextProvingPeriodEvent); + + const possessionProvenEvent2 = createPossessionProvenEvent( + SET_ID, + [ROOT_ID_1], + [BigInt.fromI32(100)], + CONTRACT_ADDRESS, + proofBlockNumber2, + BigInt.fromI32(3000) + ); + handlePossessionProven(possessionProvenEvent2); + + const fourthNextProvingPeriodEvent = createNextProvingPeriodEvent( + SET_ID, + challengeEpoch.plus(BigInt.fromI32(300)), + leafCount, + CONTRACT_ADDRESS, + fourthProvingBlockNumber, + BigInt.fromI32(3500), + generateTxHash(1004), + BigInt.fromI32(0) + ); + handleNextProvingPeriod(fourthNextProvingPeriodEvent); + + const proofSetId = getProofSetId(); + const providerId = getProviderId(); + + const expectedTotalFaultedPeriods = BigInt.fromI32(1); + + assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "4"); + assert.fieldEquals( + "DataSet", + proofSetId, + "totalFaultedPeriods", + expectedTotalFaultedPeriods.toString() + ); + + assert.fieldEquals( + "Provider", + providerId, + "totalFaultedPeriods", + expectedTotalFaultedPeriods.toString() + ); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "4"); + }); +}); diff --git a/apps/subgraph/tests/fwss-utils.ts b/apps/subgraph/tests/fwss-utils.ts new file mode 100644 index 00000000..e5db3137 --- /dev/null +++ b/apps/subgraph/tests/fwss-utils.ts @@ -0,0 +1,246 @@ +import { newMockEvent } from "matchstick-as"; +import { ethereum, BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + DataSetCreated as FwssDataSetCreated, + PieceAdded as FwssPieceAdded, + ServiceTerminated as FwssServiceTerminated, + PDPPaymentTerminated as FwssPdpPaymentTerminated, + DataSetServiceProviderChanged as FwssDataSetServiceProviderChanged, +} from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService"; + +// FWSS DataSetCreated: +// dataSetId, providerId, pdpRailId, cacheMissRailId, cdnRailId, +// payer, serviceProvider, payee, metadataKeys[], metadataValues[] +export function createFwssDataSetCreatedEvent( + dataSetId: BigInt, + providerId: BigInt, + pdpRailId: BigInt, + payer: Address, + serviceProvider: Address, + metadataKeys: string[], + metadataValues: string[], + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssDataSetCreated { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "providerId", + ethereum.Value.fromUnsignedBigInt(providerId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pdpRailId", + ethereum.Value.fromUnsignedBigInt(pdpRailId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cacheMissRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cdnRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam("payer", ethereum.Value.fromAddress(payer)) + ); + ev.parameters.push( + new ethereum.EventParam( + "serviceProvider", + ethereum.Value.fromAddress(serviceProvider) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "payee", + ethereum.Value.fromAddress(serviceProvider) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "metadataKeys", + ethereum.Value.fromStringArray(metadataKeys) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "metadataValues", + ethereum.Value.fromStringArray(metadataValues) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS PieceAdded: dataSetId, pieceId, Cids.Cid (tuple(bytes)), keys[], values[] +export function createFwssPieceAddedEvent( + dataSetId: BigInt, + pieceId: BigInt, + pieceCidBytes: Bytes, + keys: string[], + values: string[], + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssPieceAdded { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pieceId", + ethereum.Value.fromUnsignedBigInt(pieceId) + ) + ); + + let cidTuple = new ethereum.Tuple(); + cidTuple.push(ethereum.Value.fromBytes(pieceCidBytes)); + ev.parameters.push( + new ethereum.EventParam("pieceCid", ethereum.Value.fromTuple(cidTuple)) + ); + + ev.parameters.push( + new ethereum.EventParam("keys", ethereum.Value.fromStringArray(keys)) + ); + ev.parameters.push( + new ethereum.EventParam("values", ethereum.Value.fromStringArray(values)) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS ServiceTerminated: caller, dataSetId, pdpRailId, cacheMissRailId, cdnRailId +export function createFwssServiceTerminatedEvent( + dataSetId: BigInt, + caller: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssServiceTerminated { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam("caller", ethereum.Value.fromAddress(caller)) + ); + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pdpRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cacheMissRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "cdnRailId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0)) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS PDPPaymentTerminated: dataSetId, endEpoch, pdpRailId +export function createFwssPdpPaymentTerminatedEvent( + dataSetId: BigInt, + endEpoch: BigInt, + pdpRailId: BigInt, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssPdpPaymentTerminated { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "endEpoch", + ethereum.Value.fromUnsignedBigInt(endEpoch) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "pdpRailId", + ethereum.Value.fromUnsignedBigInt(pdpRailId) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} + +// FWSS DataSetServiceProviderChanged: dataSetId, oldServiceProvider, newServiceProvider +export function createFwssDataSetServiceProviderChangedEvent( + dataSetId: BigInt, + oldServiceProvider: Address, + newServiceProvider: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1) +): FwssDataSetServiceProviderChanged { + let ev = changetype(newMockEvent()); + ev.parameters = new Array(); + + ev.parameters.push( + new ethereum.EventParam( + "dataSetId", + ethereum.Value.fromUnsignedBigInt(dataSetId) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "oldServiceProvider", + ethereum.Value.fromAddress(oldServiceProvider) + ) + ); + ev.parameters.push( + new ethereum.EventParam( + "newServiceProvider", + ethereum.Value.fromAddress(newServiceProvider) + ) + ); + + ev.block.number = blockNumber; + ev.block.timestamp = timestamp; + return ev; +} diff --git a/apps/subgraph/tests/fwss.test.ts b/apps/subgraph/tests/fwss.test.ts new file mode 100644 index 00000000..6a8433ad --- /dev/null +++ b/apps/subgraph/tests/fwss.test.ts @@ -0,0 +1,455 @@ +import { + assert, + describe, + test, + clearStore, + beforeEach, +} from "matchstick-as/assembly/index"; +import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + handleDataSetCreated, + handlePiecesAdded, + getRootEntityId, +} from "../src/pdp-verifier"; +import { + handleFwssDataSetCreated, + handleFwssPieceAdded, + handleFwssServiceTerminated, + handleFwssPdpPaymentTerminated, + handleFwssDataSetServiceProviderChanged, +} from "../src/fwss"; +import { + createDataSetCreatedEvent, + createRootsAddedEvent, +} from "./pdp-verifier-utils"; +import { + createFwssDataSetCreatedEvent, + createFwssPieceAddedEvent, + createFwssServiceTerminatedEvent, + createFwssPdpPaymentTerminatedEvent, + createFwssDataSetServiceProviderChangedEvent, +} from "./fwss-utils"; + +const SET_ID = BigInt.fromI32(1); +const PROVIDER_ID = BigInt.fromI32(42); +const PDP_RAIL_ID = BigInt.fromI32(99); +const ROOT_ID = BigInt.fromI32(101); +const PROVIDER_ADDRESS = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" +); +const PAYER_ADDRESS = Address.fromString( + "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" +); +const NEW_PROVIDER_ADDRESS = Address.fromString( + "0xc16081f360e3847006db660bae1c6d1b2e17ec2c" +); +const CONTRACT_ADDRESS = Address.fromString( + "0xd16081f360e3847006db660bae1c6d1b2e17ec2d" +); + +const PROOF_SET_ENTITY_ID = Bytes.fromByteArray(Bytes.fromBigInt(SET_ID)); + +function seedDataSet(): void { + let ev = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + Bytes.fromI32(0), + CONTRACT_ADDRESS + ); + handleDataSetCreated(ev); +} + +function seedRoot(): void { + let ev = createRootsAddedEvent( + SET_ID, + [ROOT_ID], + PROVIDER_ADDRESS, + CONTRACT_ADDRESS + ); + handlePiecesAdded(ev); +} + +describe("FWSS handlers", () => { + beforeEach(() => { + clearStore(); + }); + + // -- handleFwssDataSetCreated ------------------------------------------- + + test("PDPVerifier-created DataSet has default FWSS fields", () => { + seedDataSet(); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "false" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "false" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "metadataKeys", + "[]" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "metadataValues", + "[]" + ); + }); + + test("handleFwssDataSetCreated populates FWSS fields and derives withIPFSIndexing", () => { + seedDataSet(); + let ev = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["source", "withIPFSIndexing", "withCDN"], + ["filecoin-pin", "", "true"] + ); + handleFwssDataSetCreated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssProviderId", + PROVIDER_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssPayer", + PAYER_ADDRESS.toHexString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssServiceProvider", + PROVIDER_ADDRESS.toHexString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssPdpRailId", + PDP_RAIL_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "true" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "true" + ); + }); + + test("handleFwssDataSetCreated leaves booleans false when keys absent", () => { + seedDataSet(); + let ev = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["source"], + ["filecoin-pin"] + ); + handleFwssDataSetCreated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "false" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "false" + ); + }); + + test("handleFwssDataSetCreated creates a stub when DataSet doesn't exist yet", () => { + // FWSS.DataSetCreated fires BEFORE PDPVerifier.DataSetCreated in the same + // tx (see PDPVerifier._createDataSet). When our handler runs first, it + // must create a stub with FWSS fields set so the later PDPVerifier handler + // can load it instead of overwriting. + const UNSEEN_SET_ID = BigInt.fromI32(999); + const unseenEntityId = Bytes.fromByteArray( + Bytes.fromBigInt(UNSEEN_SET_ID) + ).toHexString(); + + let ev = createFwssDataSetCreatedEvent( + UNSEEN_SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["withIPFSIndexing"], + [""] + ); + handleFwssDataSetCreated(ev); + + // Stub was created with FWSS fields populated. + assert.fieldEquals("DataSet", unseenEntityId, "setId", "999"); + assert.fieldEquals("DataSet", unseenEntityId, "fwssPayer", PAYER_ADDRESS.toHexString()); + assert.fieldEquals("DataSet", unseenEntityId, "withIPFSIndexing", "true"); + // Placeholder owner/listener set by the FWSS handler (pdp-verifier will + // overwrite when it runs later in the same block). + assert.fieldEquals("DataSet", unseenEntityId, "owner", PROVIDER_ADDRESS.toHexString()); + }); + + test("FWSS-then-PDPVerifier ordering preserves both field groups", () => { + // Simulates real on-chain ordering: FWSS.DataSetCreated fires before + // PDPVerifier.DataSetCreated. After both handlers run, FWSS and + // PDPVerifier fields must both be populated correctly. + let fwssEv = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["withIPFSIndexing", "withCDN"], + ["", "true"] + ); + handleFwssDataSetCreated(fwssEv); + + // Then PDPVerifier fires, which must load the stub (not overwrite it). + let pdpEv = createDataSetCreatedEvent( + SET_ID, + PROVIDER_ADDRESS, + Bytes.fromI32(0), + CONTRACT_ADDRESS + ); + handleDataSetCreated(pdpEv); + + // FWSS fields preserved + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssProviderId", + PROVIDER_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withIPFSIndexing", + "true" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "withCDN", + "true" + ); + // PDPVerifier fields set + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "setId", + SET_ID.toString() + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "true" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "status", + "EMPTY" + ); + }); + + // -- handleFwssPieceAdded ----------------------------------------------- + + test("PDPVerifier-created Root has default FWSS fields", () => { + seedDataSet(); + seedRoot(); + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "metadataKeys", "[]"); + assert.fieldEquals("Root", rootId, "metadataValues", "[]"); + }); + + test("handleFwssPieceAdded extracts ipfsRootCID", () => { + seedDataSet(); + seedRoot(); + let ev = createFwssPieceAddedEvent( + SET_ID, + ROOT_ID, + Bytes.fromHexString("0xdeadbeef"), + ["ipfsRootCID"], + ["bafybeiexamplecid"] + ); + handleFwssPieceAdded(ev); + + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "ipfsRootCID", "bafybeiexamplecid"); + }); + + test("handleFwssPieceAdded leaves ipfsRootCID null when absent", () => { + seedDataSet(); + seedRoot(); + let ev = createFwssPieceAddedEvent( + SET_ID, + ROOT_ID, + Bytes.fromHexString("0xdeadbeef"), + [], + [] + ); + handleFwssPieceAdded(ev); + + // When a nullable field has no value, matchstick's fieldEquals with "null" + // matches. Verify no crash and empty arrays persist. + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "metadataKeys", "[]"); + }); + + test("handleFwssPieceAdded no-ops for unknown pieceId", () => { + seedDataSet(); + // no seedRoot — root doesn't exist + let ev = createFwssPieceAddedEvent( + SET_ID, + BigInt.fromI32(999), + Bytes.fromHexString("0xdeadbeef"), + ["ipfsRootCID"], + ["bafybeinope"] + ); + handleFwssPieceAdded(ev); + + const rootId = getRootEntityId(SET_ID, BigInt.fromI32(999)).toHexString(); + assert.notInStore("Root", rootId); + }); + + // -- handleFwssServiceTerminated ---------------------------------------- + + test("handleFwssServiceTerminated flips isActive to false", () => { + seedDataSet(); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "true" + ); + + let ev = createFwssServiceTerminatedEvent(SET_ID, PROVIDER_ADDRESS); + handleFwssServiceTerminated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "false" + ); + }); + + test("handleFwssServiceTerminated no-ops for unknown dataSetId", () => { + let ev = createFwssServiceTerminatedEvent( + BigInt.fromI32(999), + PROVIDER_ADDRESS + ); + handleFwssServiceTerminated(ev); + assert.notInStore( + "DataSet", + Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + ); + }); + + // -- handleFwssPdpPaymentTerminated ------------------------------------- + + test("handleFwssPdpPaymentTerminated stores endEpoch and leaves isActive alone", () => { + seedDataSet(); + let ev = createFwssPdpPaymentTerminatedEvent( + SET_ID, + BigInt.fromI32(12345), + PDP_RAIL_ID + ); + handleFwssPdpPaymentTerminated(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "pdpPaymentEndEpoch", + "12345" + ); + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "isActive", + "true" + ); + }); + + test("handleFwssPdpPaymentTerminated no-ops for unknown dataSetId", () => { + let ev = createFwssPdpPaymentTerminatedEvent( + BigInt.fromI32(999), + BigInt.fromI32(12345), + PDP_RAIL_ID + ); + handleFwssPdpPaymentTerminated(ev); + assert.notInStore( + "DataSet", + Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + ); + }); + + // -- handleFwssDataSetServiceProviderChanged ---------------------------- + + test("handleFwssDataSetServiceProviderChanged updates fwssServiceProvider", () => { + seedDataSet(); + // seed with FWSS initial state + handleFwssDataSetCreated( + createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + [], + [] + ) + ); + + let ev = createFwssDataSetServiceProviderChangedEvent( + SET_ID, + PROVIDER_ADDRESS, + NEW_PROVIDER_ADDRESS + ); + handleFwssDataSetServiceProviderChanged(ev); + + assert.fieldEquals( + "DataSet", + PROOF_SET_ENTITY_ID.toHexString(), + "fwssServiceProvider", + NEW_PROVIDER_ADDRESS.toHexString() + ); + }); + + test("handleFwssDataSetServiceProviderChanged no-ops for unknown dataSetId", () => { + let ev = createFwssDataSetServiceProviderChangedEvent( + BigInt.fromI32(999), + PROVIDER_ADDRESS, + NEW_PROVIDER_ADDRESS + ); + handleFwssDataSetServiceProviderChanged(ev); + assert.notInStore( + "DataSet", + Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + ); + }); +}); diff --git a/apps/subgraph/tests/pdp-verifier-utils.ts b/apps/subgraph/tests/pdp-verifier-utils.ts new file mode 100644 index 00000000..d3e37a29 --- /dev/null +++ b/apps/subgraph/tests/pdp-verifier-utils.ts @@ -0,0 +1,273 @@ +import { newMockEvent } from "matchstick-as"; +import { ethereum, BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { + DataSetCreated, + PiecesAdded, + NextProvingPeriod, + PossessionProven, + DataSetDeleted, + DataSetEmpty, +} from "../generated/PDPVerifier/PDPVerifier"; + +// Helper to generate unique transaction hash from a counter +export function generateTxHash(counter: i32): Bytes { + const hexCounter = counter.toString(16).padStart(64, "0"); + return Bytes.fromHexString("0x" + hexCounter); +} + +// Mocks the DataSetCreated event +// event DataSetCreated(uint256 indexed setId, address indexed provider, bytes32 root); +export function createDataSetCreatedEvent( + setId: BigInt, + provider: Address, + root: Bytes, // Although root is part of the event, handleDataSetCreated might not use it directly + contractAddress: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1), + txHash: Bytes = generateTxHash(1), + logIndex: BigInt = BigInt.fromI32(0) +): DataSetCreated { + let DataSetCreatedEvent = changetype(newMockEvent()); + + DataSetCreatedEvent.parameters = new Array(); + + let setIdParam = new ethereum.EventParam( + "setId", + ethereum.Value.fromUnsignedBigInt(setId) + ); + let providerParam = new ethereum.EventParam( + "provider", + ethereum.Value.fromAddress(provider) + ); + let rootParam = new ethereum.EventParam( + "root", + ethereum.Value.fromFixedBytes(root) + ); + + DataSetCreatedEvent.parameters.push(setIdParam); + DataSetCreatedEvent.parameters.push(providerParam); + DataSetCreatedEvent.parameters.push(rootParam); + + DataSetCreatedEvent.address = contractAddress; + DataSetCreatedEvent.block.number = blockNumber; + DataSetCreatedEvent.block.timestamp = timestamp; + DataSetCreatedEvent.transaction.hash = txHash; + DataSetCreatedEvent.logIndex = logIndex; + + // Transaction input is not strictly needed if the handler only uses event.params + // DataSetCreatedEvent.transaction.input = Bytes.fromI32(0); + + return DataSetCreatedEvent; +} + +export function createRootsAddedEvent( + setId: BigInt, + pieceIds: BigInt[], + sender: Address, + contractAddress: Address +): PiecesAdded { + let rootsAddedEvent = changetype(newMockEvent()); + + rootsAddedEvent.parameters = new Array(); + rootsAddedEvent.address = contractAddress; + rootsAddedEvent.transaction.from = sender; + rootsAddedEvent.transaction.to = contractAddress; + + let setIdParam = new ethereum.EventParam( + "setId", + ethereum.Value.fromUnsignedBigInt(setId) + ); + let rootIdsParam = new ethereum.EventParam( + "pieceIds", + ethereum.Value.fromUnsignedBigIntArray(pieceIds) + ); + + let pieceCids: Array = []; + for (let i = 0; i < pieceIds.length; i++) { + let cidTuple = new ethereum.Tuple(); + let cidData = Bytes.fromHexString( + "0x01559120258ff7f7021387dcea7164b7d1c4a98bd6f8d3c187e3114795efa391df307c8aa9d5d5cbac03" + ); + cidTuple.push(ethereum.Value.fromBytes(cidData)); + pieceCids.push(cidTuple); + } + + let pieceCidsParam = new ethereum.EventParam( + "pieceCids", + ethereum.Value.fromTupleArray(pieceCids) + ); + + rootsAddedEvent.parameters.push(setIdParam); + rootsAddedEvent.parameters.push(rootIdsParam); + rootsAddedEvent.parameters.push(pieceCidsParam); + + let txInputHex = + "0x9afd37f20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002a01559120258ff7f7021387dcea7164b7d1c4a98bd6f8d3c187e3114795efa391df307c8aa9d5d5cbac030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a05f13cbf0c320f1092664967af5de13e4abe964d4f755c0d4cffe18a146f395030000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b69706673526f6f744349440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003b626166796265696537696d32766e766870347a726d6778776c6b336d6133736f736f6e743765367776726f63336134756261707a7a7a3368796a75000000000000000000000000000000000000000000000000000000000000000000000000411b4c7e389fe7383d20d251599c194c9ddb3e71d79c2c1b44fe15b0f505aea92e525239a7e91647c64370054fe8a779486342fafb8971a7eb69101c97368c4bf61b00000000000000000000000000000000000000000000000000000000000000"; + let txInput = Bytes.fromHexString(txInputHex); + rootsAddedEvent.transaction.input = txInput; + + rootsAddedEvent.block.number = BigInt.fromI32(1); + rootsAddedEvent.block.timestamp = BigInt.fromI32(1); + + return rootsAddedEvent; +} + +export function createNextProvingPeriodEvent( + setId: BigInt, + challengeEpoch: BigInt, + leafCount: BigInt, + contractAddress: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1), + txHash: Bytes = generateTxHash(2), + logIndex: BigInt = BigInt.fromI32(0) +): NextProvingPeriod { + let nextProvingPeriodEvent = changetype(newMockEvent()); + + nextProvingPeriodEvent.parameters = new Array(); + + let setIdParam = new ethereum.EventParam( + "setId", + ethereum.Value.fromUnsignedBigInt(setId) + ); + let challengeEpochParam = new ethereum.EventParam( + "challengeEpoch", + ethereum.Value.fromUnsignedBigInt(challengeEpoch) + ); + let leafCountParam = new ethereum.EventParam( + "leafCount", + ethereum.Value.fromUnsignedBigInt(leafCount) + ); + + nextProvingPeriodEvent.parameters.push(setIdParam); + nextProvingPeriodEvent.parameters.push(challengeEpochParam); + nextProvingPeriodEvent.parameters.push(leafCountParam); + + nextProvingPeriodEvent.address = contractAddress; + nextProvingPeriodEvent.block.number = blockNumber; + nextProvingPeriodEvent.block.timestamp = timestamp; + nextProvingPeriodEvent.transaction.hash = txHash; + nextProvingPeriodEvent.logIndex = logIndex; + + return nextProvingPeriodEvent; +} + +export function createPossessionProvenEvent( + setId: BigInt, + pieceIds: BigInt[], + offsets: BigInt[], + contractAddress: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1), + txHash: Bytes = generateTxHash(3), + logIndex: BigInt = BigInt.fromI32(0) +): PossessionProven { + if (pieceIds.length !== offsets.length) { + throw new Error( + `createPossessionProvenEvent: pieceIds.length (${pieceIds.length}) must equal offsets.length (${offsets.length})` + ); + } + + let possessionProvenEvent = changetype(newMockEvent()); + + possessionProvenEvent.parameters = new Array(); + + let setIdParam = new ethereum.EventParam( + "setId", + ethereum.Value.fromUnsignedBigInt(setId) + ); + + let challenges: Array = []; + for (let i = 0; i < pieceIds.length; i++) { + let challenge = new ethereum.Tuple(); + challenge.push(ethereum.Value.fromUnsignedBigInt(pieceIds[i])); + challenge.push(ethereum.Value.fromUnsignedBigInt(offsets[i])); + challenges.push(challenge); + } + + let challengesParam = new ethereum.EventParam( + "challenges", + ethereum.Value.fromTupleArray(challenges) + ); + + possessionProvenEvent.parameters.push(setIdParam); + possessionProvenEvent.parameters.push(challengesParam); + + possessionProvenEvent.address = contractAddress; + possessionProvenEvent.block.number = blockNumber; + possessionProvenEvent.block.timestamp = timestamp; + possessionProvenEvent.transaction.hash = txHash; + possessionProvenEvent.logIndex = logIndex; + + return possessionProvenEvent; +} + +export function createDataSetDeletedEvent( + setId: BigInt, + deletedLeafCount: BigInt, + contractAddress: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1), + txHash: Bytes = generateTxHash(4), + logIndex: BigInt = BigInt.fromI32(0) +): DataSetDeleted { + let dataSetDeletedEvent = changetype(newMockEvent()); + + dataSetDeletedEvent.parameters = new Array(); + + let setIdParam = new ethereum.EventParam( + "setId", + ethereum.Value.fromUnsignedBigInt(setId) + ); + let deletedLeafCountParam = new ethereum.EventParam( + "deletedLeafCount", + ethereum.Value.fromUnsignedBigInt(deletedLeafCount) + ); + + dataSetDeletedEvent.parameters.push(setIdParam); + dataSetDeletedEvent.parameters.push(deletedLeafCountParam); + + dataSetDeletedEvent.address = contractAddress; + dataSetDeletedEvent.block.number = blockNumber; + dataSetDeletedEvent.block.timestamp = timestamp; + dataSetDeletedEvent.transaction.hash = txHash; + dataSetDeletedEvent.logIndex = logIndex; + dataSetDeletedEvent.transaction.from = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" + ); + dataSetDeletedEvent.transaction.to = contractAddress; + + return dataSetDeletedEvent; +} + +export function createDataSetEmptyEvent( + setId: BigInt, + contractAddress: Address, + blockNumber: BigInt = BigInt.fromI32(1), + timestamp: BigInt = BigInt.fromI32(1), + txHash: Bytes = generateTxHash(5), + logIndex: BigInt = BigInt.fromI32(0) +): DataSetEmpty { + let dataSetEmptyEvent = changetype(newMockEvent()); + + dataSetEmptyEvent.parameters = new Array(); + + let setIdParam = new ethereum.EventParam( + "setId", + ethereum.Value.fromUnsignedBigInt(setId) + ); + + dataSetEmptyEvent.parameters.push(setIdParam); + + dataSetEmptyEvent.address = contractAddress; + dataSetEmptyEvent.block.number = blockNumber; + dataSetEmptyEvent.block.timestamp = timestamp; + dataSetEmptyEvent.transaction.hash = txHash; + dataSetEmptyEvent.logIndex = logIndex; + dataSetEmptyEvent.transaction.from = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" + ); + dataSetEmptyEvent.transaction.to = contractAddress; + + return dataSetEmptyEvent; +} diff --git a/apps/subgraph/tests/pdp-verifier.test.ts b/apps/subgraph/tests/pdp-verifier.test.ts new file mode 100644 index 00000000..9f117cfa --- /dev/null +++ b/apps/subgraph/tests/pdp-verifier.test.ts @@ -0,0 +1,150 @@ +import { + assert, + describe, + test, + clearStore, + beforeAll, + afterAll, +} from "matchstick-as/assembly/index"; +import { BigInt, Address, Bytes, ByteArray } from "@graphprotocol/graph-ts"; +import { + handlePiecesAdded, + handleDataSetCreated, + getRootEntityId, +} from "../src/pdp-verifier"; +import { + createRootsAddedEvent, + createDataSetCreatedEvent, +} from "./pdp-verifier-utils"; + +// Define constants for test data +const SET_ID = BigInt.fromI32(1); +const ROOT_ID_1 = BigInt.fromI32(101); +const RAW_SIZE_1 = BigInt.fromI32(10486897); +// CIDs as strings +const ROOT_CID_1_STR = + "0x01559120258ff7f7021387dcea7164b7d1c4a98bd6f8d3c187e3114795efa391df307c8aa9d5d5cbac03"; +const SENDER_ADDRESS = Address.fromString( + "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" +); +const LISTENER_ADDRESS = Address.fromString( + "0x0000000000000000000000000000000000000001" +); +const CONTRACT_ADDRESS = Address.fromString( + "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" +); +const PROOF_SET_ID_BYTES = Bytes.fromBigInt(SET_ID); + +// Helper to convert string to Bytes and pad to 32 bytes +function stringToBytes32(str: string): Bytes { + let utf8Bytes = Bytes.fromUTF8(str); + let paddedBytes = new ByteArray(32); // Create a 32-byte array, initialized to zeros + + // Copy bytes from utf8Bytes, ensuring we don't exceed 32 bytes + for (let i = 0; i < utf8Bytes.length && i < 32; i++) { + paddedBytes[i] = utf8Bytes[i]; + } + return Bytes.fromByteArray(paddedBytes); +} + +describe("handlePiecesAdded Tests", () => { + beforeAll(() => { + // 1. Create the necessary DataSet first + let mockDataSetCreatedEvent = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), // Dummy root, as it's required by the function but not used by the handler here + CONTRACT_ADDRESS, + BigInt.fromI32(50), // Match block number for consistency + BigInt.fromI32(1678886400) // Match timestamp for consistency + ); + handleDataSetCreated(mockDataSetCreatedEvent); + + // 2. Create and handle the piecesAdded event + let pieceIds = [ROOT_ID_1]; + let rootsAddedEvent = createRootsAddedEvent( + SET_ID, + pieceIds, + SENDER_ADDRESS, + CONTRACT_ADDRESS + ); + + // Set block/tx details on the mock event if needed by handler + rootsAddedEvent.block.timestamp = BigInt.fromI32(100); // Example timestamp + rootsAddedEvent.block.number = BigInt.fromI32(50); // Example block number + rootsAddedEvent.logIndex = BigInt.fromI32(1); // Example log index + rootsAddedEvent.transaction.hash = Bytes.fromHexString( + "0x" + "c".repeat(64) + ); + + handlePiecesAdded(rootsAddedEvent); + }); + + afterAll(() => { + clearStore(); + }); + + test("Entities created and stored correctly", () => { + // Assert counts + assert.entityCount("DataSet", 1); + assert.entityCount("Root", 1); // One root was added + assert.entityCount("Provider", 1); + assert.entityCount("EventLog", 2); // piecesAdded creates one event log + + // --- Assert DataSet fields --- + let dataSetId = PROOF_SET_ID_BYTES.toHex(); + assert.fieldEquals("DataSet", dataSetId, "setId", SET_ID.toString()); + assert.fieldEquals("DataSet", dataSetId, "totalRoots", "1"); // Initially 0, added 1 + let expectedTotalSize = RAW_SIZE_1.toString(); + assert.fieldEquals( + "DataSet", + dataSetId, + "totalDataSize", + expectedTotalSize + ); + assert.fieldEquals("DataSet", dataSetId, "updatedAt", "100"); + assert.fieldEquals("DataSet", dataSetId, "blockNumber", "50"); + + // --- Assert Root fields --- + let rootEntityId1 = getRootEntityId(SET_ID, ROOT_ID_1).toHex(); + assert.fieldEquals("Root", rootEntityId1, "rootId", ROOT_ID_1.toString()); + assert.fieldEquals("Root", rootEntityId1, "setId", SET_ID.toString()); + assert.fieldEquals("Root", rootEntityId1, "cid", ROOT_CID_1_STR); + assert.fieldEquals("Root", rootEntityId1, "rawSize", RAW_SIZE_1.toString()); + // assert.fieldEquals("Root", rootEntityId1, "createdAt", "100"); + assert.fieldEquals("Root", rootEntityId1, "blockNumber", "50"); + + // --- Assert Provider fields --- + let providerId = SENDER_ADDRESS.toHex(); + assert.fieldEquals( + "Provider", + providerId, + "totalDataSize", + expectedTotalSize + ); + assert.fieldEquals("Provider", providerId, "updatedAt", "100"); + assert.fieldEquals("Provider", providerId, "blockNumber", "50"); + // Assuming provider was newly created by this event + // assert.fieldEquals("Provider", providerId, "createdAt", "100"); + + // --- Assert EventLog fields --- + // Construct expected event ID: txHash + logIndex + let eventId = Bytes.fromHexString("0x" + "c".repeat(64)) + .concatI32(BigInt.fromI32(1).toI32()) + .toHex(); + assert.fieldEquals("EventLog", eventId, "name", "piecesAdded"); + assert.fieldEquals("EventLog", eventId, "setId", SET_ID.toString()); + assert.fieldEquals( + "EventLog", + eventId, + "transactionHash", + "0x" + "c".repeat(64) + ); + assert.fieldEquals("EventLog", eventId, "blockNumber", "50"); + assert.fieldEquals("EventLog", eventId, "logIndex", "1"); + assert.fieldEquals("EventLog", eventId, "createdAt", "100"); + // Check data field (simple representation) + let expectedData = `{ "setId": "${SET_ID.toString()}", "pieceIds": [${ROOT_ID_1.toString()}] }`; + assert.fieldEquals("EventLog", eventId, "data", expectedData); + }); +}); diff --git a/apps/subgraph/tsconfig.json b/apps/subgraph/tsconfig.json new file mode 100644 index 00000000..4e866720 --- /dev/null +++ b/apps/subgraph/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@graphprotocol/graph-ts/types/tsconfig.base.json", + "include": ["src", "tests"] +} diff --git a/apps/subgraph/utils/cid.ts b/apps/subgraph/utils/cid.ts new file mode 100644 index 00000000..95a577d5 --- /dev/null +++ b/apps/subgraph/utils/cid.ts @@ -0,0 +1,118 @@ +import { Bytes, BigInt } from "@graphprotocol/graph-ts"; + +export const COMMP_V2_PREFIX: u8[] = [0x01, 0x55, 0x91, 0x20]; + +export class CommPv2ValidationResult { + constructor( + public isValid: boolean, + public padding: BigInt = BigInt.zero(), + public height: u8 = 0, + public digestOffset: BigInt = BigInt.zero(), + ) {} +} + +export class UvarintResult { + constructor( + public isValid: boolean, + public value: BigInt = BigInt.zero(), + public offset: BigInt = BigInt.zero(), + ) {} +} + +export function readUvarint(data: Bytes, offset: BigInt): UvarintResult { + let offsetU32 = offset.toU32(); + + if (offsetU32 >= u32(data.length)) { + return new UvarintResult(false); + } + + let i: u32 = 0; + let value: u64 = u64(data[offsetU32] & 0x7f); + + while (data[offsetU32 + i] >= 0x80) { + i++; + + if (offsetU32 + i >= u32(data.length)) { + return new UvarintResult(false); + } + + if (i >= 10) { + return new UvarintResult(false); + } + + let nextByte = u64(data[offsetU32 + i] & 0x7f); + value = value | (nextByte << (i * 7)); + } + + i++; + return new UvarintResult(true, BigInt.fromU64(value), BigInt.fromU32(offsetU32 + i)); +} + +export function validateCommPv2(cidData: Bytes): CommPv2ValidationResult { + if (cidData.length < 4) { + return new CommPv2ValidationResult(false); + } + + for (let i: i32 = 0; i < 4; i++) { + if (cidData[i] != COMMP_V2_PREFIX[i]) { + return new CommPv2ValidationResult(false); + } + } + + let offset = BigInt.fromU32(4); + + if (offset.toU32() >= u32(cidData.length)) { + return new CommPv2ValidationResult(false); + } + + let mhLengthResult = readUvarint(cidData, offset); + if (!mhLengthResult.isValid) { + return new CommPv2ValidationResult(false); + } + + let mhLength = mhLengthResult.value; + offset = mhLengthResult.offset; + + if (mhLength.lt(BigInt.fromU32(34))) { + return new CommPv2ValidationResult(false); + } + + if (mhLength.plus(offset).notEqual(BigInt.fromU32(cidData.length))) { + return new CommPv2ValidationResult(false); + } + + if (offset.toU32() >= u32(cidData.length)) { + return new CommPv2ValidationResult(false); + } + + let paddingResult = readUvarint(cidData, offset); + if (!paddingResult.isValid) { + return new CommPv2ValidationResult(false); + } + + let padding = paddingResult.value; + offset = paddingResult.offset; + + if (offset.toU32() >= u32(cidData.length)) { + return new CommPv2ValidationResult(false); + } + + let height = cidData[offset.toU32()]; + offset = offset.plus(BigInt.fromU32(1)); + + return new CommPv2ValidationResult(true, padding, height, offset); +} + +export function unpaddedSize(padding: BigInt, height: u8): BigInt { + if (height > 58) { + return BigInt.zero(); + } + + const baseSize = BigInt.fromU32(127).leftShift(height - 2); + + if (padding.gt(baseSize)) { + return BigInt.zero(); + } + + return baseSize.minus(padding); +} diff --git a/apps/subgraph/utils/index.ts b/apps/subgraph/utils/index.ts new file mode 100644 index 00000000..d38b6283 --- /dev/null +++ b/apps/subgraph/utils/index.ts @@ -0,0 +1,15 @@ +export const PDPVerifierAddress = "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C"; + +export const NumChallenges = 5; + +export const LeafSize = 32; + +// Proving period configuration per network. +// calibration: MaxProvingPeriod=240 +// mainnet: MaxProvingPeriod=2880 +export const MaxProvingPeriod = 240; +export const ChallengeWindowSize = 20; + +// Maximum ProvingWindow entities created per NextProvingPeriod event. +// Prevents OOM when a stale dataset resumes after many skipped periods. +export const MaxProvingWindowsPerEvent = 50; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 617eed44..f3a78e03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,27 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -catalogs: - default: - '@biomejs/biome': - specifier: 2.3.14 - version: 2.3.14 - '@swc/core': - specifier: 1.15.11 - version: 1.15.11 - '@vitest/coverage-v8': - specifier: 4.0.18 - version: 4.0.18 - typescript: - specifier: 5.9.3 - version: 5.9.3 - unplugin-swc: - specifier: 1.5.9 - version: 1.5.9 - vitest: - specifier: 4.0.18 - version: 4.0.18 - overrides: cron: 4.4.0 @@ -70,13 +49,13 @@ importers: version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) '@nestjs/schedule': specifier: ^6.1.1 - version: 6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + version: 6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/swagger': specifier: ^11.2.6 - version: 11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.18.0)(ts-node@10.9.2(@swc/core@1.15.11)(@types/node@25.2.3)(typescript@5.9.3))) + version: 11.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.18.0)(ts-node@10.9.2(@swc/core@1.15.11)(@types/node@25.2.3)(typescript@5.9.3))) '@willsoto/nestjs-prometheus': specifier: ^6.0.2 version: 6.0.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) @@ -100,7 +79,7 @@ importers: version: 4.4.0 filecoin-pin: specifier: ^0.20.0 - version: 0.20.0(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3)(zod@4.3.6) + version: 0.20.0(encoding@0.1.13)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3)(zod@4.3.6) helmet: specifier: ^8.1.0 version: 8.1.0 @@ -155,7 +134,7 @@ importers: version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.1.13 - version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13) + version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)) '@swc/core': specifier: 'catalog:' version: 1.15.11 @@ -193,6 +172,22 @@ importers: specifier: 'catalog:' version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(msw@2.12.10(@types/node@25.2.3)(typescript@5.9.3))(terser@5.46.0)(yaml@2.8.2) + apps/subgraph: + dependencies: + '@graphprotocol/graph-cli': + specifier: 0.98.1 + version: 0.98.1(@types/node@25.2.3)(typescript@5.9.3)(zod@3.25.76) + '@graphprotocol/graph-ts': + specifier: 0.38.2 + version: 0.38.2 + devDependencies: + assemblyscript: + specifier: 0.19.23 + version: 0.19.23 + matchstick-as: + specifier: 0.6.0 + version: 0.6.0 + apps/web: dependencies: '@radix-ui/react-slot': @@ -811,6 +806,9 @@ packages: '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -841,6 +839,18 @@ packages: peerDependencies: viem: 2.x + '@float-capital/float-subgraph-uncrashable@0.0.0-internal-testing.5': + resolution: {integrity: sha512-yZ0H5e3EpAYKokX/AbtplzlvSxEJY7ZfpvQyDzyODkks0hakAAlDG6fQu1SlDJMWorY7bbq1j7fCiFeTWci6TA==} + hasBin: true + + '@graphprotocol/graph-cli@0.98.1': + resolution: {integrity: sha512-GrWFcRCBlLcRT+gIGundQl7yyrX3YWUPj66bxThKf5CJvvWXdZoNxrj27dMMqulsSwYmpCkb3YmpCiVJFGdpHw==} + engines: {node: '>=20.18.1'} + hasBin: true + + '@graphprotocol/graph-ts@0.38.2': + resolution: {integrity: sha512-87KIFSFs2+Te+mnmb7Y+M57oqzlLy20cIyPIRbn9qJfpZFSZHTKtBLT6KQmcsK0YkoWis9Ur3c3M2c9mmaaEHQ==} + '@hapi/address@5.1.1': resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} engines: {node: '>=14.0.0'} @@ -1044,18 +1054,14 @@ packages: '@ipshipyard/libp2p-auto-tls@2.0.1': resolution: {integrity: sha512-zpDXVMY1ZgB6o30zFocXUzrD9+tz1bbEdgewFoBf4olDh5/CwjDi/k9v2RrJqujWKYWyRuHRg6Q+VRpvtGrpuw==} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@isaacs/ttlcache@1.4.1': resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} @@ -1156,6 +1162,9 @@ packages: '@libp2p/interface-internal@3.0.13': resolution: {integrity: sha512-qZTn1CKOro/1m8Eizb/B1pUvW/eJe5KhP/dvqKETqka26qH89eX5SlTS1OPTINXzJvfbnDFptVJOPxmpa3BfgA==} + '@libp2p/interface@2.11.0': + resolution: {integrity: sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q==} + '@libp2p/interface@3.1.0': resolution: {integrity: sha512-RE7/XyvC47fQBe1cHxhMvepYKa5bFCUyFrrpj8PuM0E7JtzxU7F+Du5j4VXbg2yLDcToe0+j8mB7jvwE2AThYw==} @@ -1165,6 +1174,9 @@ packages: '@libp2p/keychain@6.0.9': resolution: {integrity: sha512-gO8krY3iPbXzc+LLA2haTEKbnINpx/p/FlXeHZsyXSD5Q31aV2zQsOrNlVaCyDhKqb1uiyon7NMMPn8UUqkJWQ==} + '@libp2p/logger@5.2.0': + resolution: {integrity: sha512-OEFS529CnIKfbWEHmuCNESw9q0D0hL8cQ8klQfjIVPur15RcgAEgc1buQ7Y6l0B6tCYg120bp55+e9tGvn8c0g==} + '@libp2p/logger@6.2.2': resolution: {integrity: sha512-XtanXDT+TuMuZoCK760HGV1AmJsZbwAw5AiRUxWDbsZPwAroYq64nb41AHRu9Gyc0TK9YD+p72+5+FIxbw0hzw==} @@ -1180,6 +1192,9 @@ packages: '@libp2p/peer-collections@7.0.13': resolution: {integrity: sha512-SwNQFT0tfSyfbdUUKZFzHv9DXxsabuT99ch/40as8qC7xgoJJfUmhoa9FSuAuABdpTVHDJmxCI2pIbcb1kBqfg==} + '@libp2p/peer-id@5.1.9': + resolution: {integrity: sha512-cVDp7lX187Epmi/zr0Qq2RsEMmueswP9eIxYSFoMcHL/qcvRFhsxOfUGB8361E26s2WJvC9sXZ0oJS9XVueJhQ==} + '@libp2p/peer-id@6.0.4': resolution: {integrity: sha512-Z3xK0lwwKn4bPg3ozEpPr1HxsRi2CxZdghOL+MXoFah/8uhJJHxHFA8A/jxtKn4BB8xkk6F8R5vKNIS05yaCYw==} @@ -1230,9 +1245,15 @@ packages: '@multiformats/multiaddr-matcher@3.0.1': resolution: {integrity: sha512-jvjwzCPysVTQ53F4KqwmcqZw73BqHMk0UUZrMP9P4OtJ/YHrfs122ikTqhVA2upe0P/Qz9l8HVlhEifVYB2q9A==} + '@multiformats/multiaddr-to-uri@11.0.2': + resolution: {integrity: sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg==} + '@multiformats/multiaddr-to-uri@12.0.0': resolution: {integrity: sha512-3uIEBCiy8tfzxYYBl81x1tISiNBQ7mHU4pGjippbJRoQYHzy/ZdZM/7JvTldr8pc/dzpkaNJxnsuxxlhsPOJsA==} + '@multiformats/multiaddr@12.5.1': + resolution: {integrity: sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==} + '@multiformats/multiaddr@13.0.1': resolution: {integrity: sha512-XToN915cnfr6Lr9EdGWakGJbPT0ghpg/850HvdC+zFX8XvpLZElwa8synCiwa8TuvKNnny6m8j8NVBNCxhIO3g==} @@ -1377,6 +1398,9 @@ packages: resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} engines: {node: '>= 20.19.0'} + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + '@noble/curves@1.9.1': resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} @@ -1389,6 +1413,10 @@ packages: resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} engines: {node: '>= 20.19.0'} + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -1414,6 +1442,26 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@oclif/core@4.10.5': + resolution: {integrity: sha512-qcdCF7NrdWPfme6Kr34wwljRCXbCVpL1WVxiNy0Ep6vbWKjxAjFQwuhqkoyL0yjI+KdwtLcOCGn5z2yzdijc8w==} + engines: {node: '>=18.0.0'} + + '@oclif/core@4.5.5': + resolution: {integrity: sha512-iQzlaJQgPeUXrtrX71OzDwxPikQ7c2FhNd8U8rBB7BCtj2XYfmzBT/Hmbc+g9OKDIG/JkbJT0fXaWMMBrhi+1A==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-autocomplete@3.2.45': + resolution: {integrity: sha512-ENrUg8rbVCjh40uvi3MC9kGbiUoEf11nyqE59RBzegeeLpRXNo/Zp27L9j1tUmPEqGgfS2/wvHPihNzkpK1FDw==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-not-found@3.2.80': + resolution: {integrity: sha512-yTLjWvR1r/Rd/cO2LxHdMCDoL5sQhBYRUcOMCmxZtWVWhx4rAZ8KVUPDVsb+SvjJDV5ADTDBgt1H52fFx7YWqg==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-warn-if-update-available@3.1.60': + resolution: {integrity: sha512-cRKBZm14IuA6G8W84dfd3iXj3BTAoxQ5o3pUE8DKEQ4n/tVha20t5nkVeD+ISC68e0Fuw5koTMvRwXb1lJSnzg==} + engines: {node: '>=18.0.0'} + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -1672,6 +1720,9 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} + '@pinax/graph-networks-registry@0.7.1': + resolution: {integrity: sha512-Gn2kXRiEd5COAaMY/aDCRO0V+zfb1uQKCu5HFPoWka+EsZW27AlTINA7JctYYYEMuCbjMia5FBOzskjgEvj6LA==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1679,6 +1730,18 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@3.0.2': + resolution: {integrity: sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==} + engines: {node: '>=12'} + '@prisma/instrumentation@7.4.2': resolution: {integrity: sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg==} peerDependencies: @@ -1861,6 +1924,9 @@ packages: react-redux: optional: true + '@rescript/std@9.0.0': + resolution: {integrity: sha512-zGzFsgtZ44mgL4Xef2gOy1hrRVdrs9mcxCOOKZrIPsmbZW14yTkaF591GXxpQvjXiHtgZ/iA9qLyWH6oSReIxQ==} + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -1997,12 +2063,21 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + '@scure/base@1.2.6': resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + '@scure/bip32@1.7.0': resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} @@ -2399,9 +2474,15 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@25.2.3': resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/pg-pool@2.0.7': resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} @@ -2455,6 +2536,9 @@ packages: '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -2553,6 +2637,22 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@whatwg-node/disposablestack@0.0.6': + resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/fetch@0.10.13': + resolution: {integrity: sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/node-fetch@0.8.5': + resolution: {integrity: sha512-4xzCl/zphPqlp9tASLVeUhB5+WJHbuWGYpfoC2q1qh5dw0AqZBW7L27V5roxYWijPxj4sspRAAoOH3d2ztaHUQ==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/promise-helpers@1.3.2': + resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} + engines: {node: '>=16.0.0'} + '@willsoto/nestjs-prometheus@6.0.2': resolution: {integrity: sha512-ePyLZYdIrOOdlOWovzzMisIgviXqhPVzFpSMKNNhn6xajhRHeBsjAzSdpxZTc6pnjR9hw1lNAHyKnKl7lAPaVg==} peerDependencies: @@ -2565,6 +2665,15 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abitype@0.7.1: + resolution: {integrity: sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ==} + peerDependencies: + typescript: '>=4.9.4' + zod: ^3 >=3.19.1 + peerDependenciesMeta: + zod: + optional: true + abitype@1.2.3: resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} peerDependencies: @@ -2661,6 +2770,14 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2669,6 +2786,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -2681,6 +2802,10 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -2693,6 +2818,12 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + apisauce@2.1.6: + resolution: {integrity: sha512-MdxR391op/FucS2YQRfB/NMRyCnHEPDd4h17LRIuVYi0BpGmMhpxc0shbOpfs5ahABuBEffNCGal5EcsydbBWg==} + + app-module-path@2.2.0: + resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==} + app-root-path@3.1.0: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} @@ -2722,6 +2853,15 @@ packages: resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} engines: {node: '>=12.0.0'} + assemblyscript@0.19.23: + resolution: {integrity: sha512-fwOQNZVTMga5KRsfY80g7cpOl4PsFQczMwHzdtgoqLXaYhkhavufKb0sB0l3T1DUxpAufA0KNhlbpuuhZUwxMA==} + hasBin: true + + assemblyscript@0.27.31: + resolution: {integrity: sha512-Ra8kiGhgJQGZcBxjtMcyVRxOEJZX64kd+XGpjWzjcjgxWJVv+CAQO0aDBk4GQVhjYbOkATarC83mHjAVGtwPBQ==} + engines: {node: '>=16', npm: '>=7'} + hasBin: true + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2729,6 +2869,9 @@ packages: ast-v8-to-istanbul@0.3.11: resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2746,6 +2889,9 @@ packages: avvio@9.1.0: resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + axios@0.21.4: + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + axios@1.13.5: resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} @@ -2794,15 +2940,29 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binaryen@102.0.0-nightly.20211028: + resolution: {integrity: sha512-GCJBVB5exbxzzvyt8MGDv/MeUjs6gkXDvf4xOIItRBptYl0Tz5sm1o/uG95YK0L0VeG5ajDu3hRtkBP2kzqC5w==} + hasBin: true + + binaryen@116.0.0-nightly.20240114: + resolution: {integrity: sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==} + hasBin: true + bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@1.2.3: + resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} bl@5.1.0: resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + blob-to-it@2.0.12: + resolution: {integrity: sha512-0zEZt8t8/QrdH4boktG19F/9fqfPWFjuh1QlK0qTCO13oUWaBAR8kpNloQNb3OWUtaA0mu8qfPy0R3CZDC8M2g==} + blockstore-core@6.1.2: resolution: {integrity: sha512-yWU38RM8DJ6C7Y2shIeTNVgGiJX/ko2RXqDyNlxMakOc+aVS7k1SCiakMlh6ix0juRNPtj0ySMTXU8UBDXXRCQ==} @@ -2820,6 +2980,10 @@ packages: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2835,6 +2999,18 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2844,6 +3020,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2887,6 +3067,10 @@ packages: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2932,6 +3116,10 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + clean-stack@3.0.1: + resolution: {integrity: sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==} + engines: {node: '>=10'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -2940,6 +3128,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-table3@0.6.0: + resolution: {integrity: sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==} + engines: {node: 10.* || >= 12.*} + cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -2964,13 +3156,23 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3008,6 +3210,9 @@ packages: resolution: {integrity: sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==} engines: {node: '>=20'} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + connect@3.7.0: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} @@ -3053,6 +3258,10 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cosmiconfig@7.0.1: + resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} + engines: {node: '>=10'} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -3073,6 +3282,10 @@ packages: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} engines: {node: '>=18.x'} + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3135,6 +3348,9 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + dag-jose@5.1.1: + resolution: {integrity: sha512-9alfZ8Wh1XOOMel8bMpDqWsDT72ojFQCJPtwZSev9qh4f8GoCV9qrJW8jcOUhcstO8Kfm09FHGo//jqiZq3z9w==} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3191,6 +3407,26 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + decompress-tar@4.1.1: + resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==} + engines: {node: '>=4'} + + decompress-tarbz2@4.1.1: + resolution: {integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==} + engines: {node: '>=4'} + + decompress-targz@4.1.1: + resolution: {integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==} + engines: {node: '>=4'} + + decompress-unzip@4.0.1: + resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==} + engines: {node: '>=4'} + + decompress@4.2.1: + resolution: {integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==} + engines: {node: '>=4'} + dedent@1.7.0: resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} peerDependencies: @@ -3207,6 +3443,14 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -3214,6 +3458,14 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + delay@6.0.0: resolution: {integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==} engines: {node: '>=16'} @@ -3259,6 +3511,10 @@ packages: dnum@2.17.0: resolution: {integrity: sha512-Abo8RU2ZoABVO2R051XlJEgDIXAlA8/ZjOT2F1uAWvm6Vb8TphmN4k7qgu5nWKSv/JUGLVty6QPEeLTvaxNRYQ==} + docker-compose@1.3.0: + resolution: {integrity: sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==} + engines: {node: '>= 6.0.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -3291,6 +3547,20 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + ejs@3.1.8: + resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-fetch@1.9.1: + resolution: {integrity: sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA==} + engines: {node: '>=6'} + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -3308,6 +3578,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -3315,6 +3588,10 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -3357,6 +3634,12 @@ packages: es-toolkit@1.44.0: resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + esbuild@0.27.1: resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} engines: {node: '>=18'} @@ -3369,6 +3652,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -3408,6 +3695,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -3426,6 +3716,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -3441,12 +3735,19 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3457,6 +3758,9 @@ packages: fast-json-stringify@6.1.1: resolution: {integrity: sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==} + fast-levenshtein@3.0.0: + resolution: {integrity: sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==} + fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} @@ -3466,6 +3770,10 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastify@5.8.2: resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} @@ -3480,6 +3788,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3493,10 +3804,25 @@ packages: resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} + file-type@3.9.0: + resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} + engines: {node: '>=0.10.0'} + + file-type@5.2.0: + resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==} + engines: {node: '>=4'} + + file-type@6.2.0: + resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==} + engines: {node: '>=4'} + filecoin-pin@0.20.0: resolution: {integrity: sha512-rWYJuW0B75LUMvUiUUR6F3iQmHdwVdV0RrmG3sljP3+gVCboYkc0xB80RWlT631xCDCncz/lmstBAQ9r+RbdKw==} hasBin: true + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -3581,6 +3907,13 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fs-jetpack@4.3.1: + resolution: {integrity: sha512-dbeOK84F6BiQzk2yqqCVwCPWTxAvVGJ3fMQc6E2wuEohS28mR6yHngbrKuVCK1KHRx/ccByDylqu4H5PCP2urQ==} + fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} @@ -3599,6 +3932,10 @@ packages: resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==} engines: {node: '>=14.16'} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3611,6 +3948,9 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-iterator@1.0.2: + resolution: {integrity: sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==} + get-iterator@2.0.1: resolution: {integrity: sha512-7HuY/hebu4gryTDT7O/XY/fvY9wRByEGdK6QOa4of8npTcv0+NS6frFKABcf6S9EBAsveTuKTsZQQBFMMNILIg==} @@ -3626,6 +3966,14 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@2.3.1: + resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} + engines: {node: '>=0.10.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -3641,6 +3989,12 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.0: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} @@ -3649,13 +4003,29 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + gluegun@5.2.0: + resolution: {integrity: sha512-jSUM5xUy2ztYFQANne17OUm/oAd7qSX7EBksS9bQDt9UvLPqcEkeWUebmaposb8Tx7eTTD8uJVWGRe6PYSsYkg==} + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql-import-node@0.0.5: + resolution: {integrity: sha512-OXbou9fqh9/Lm7vwXT0XoRN9J5+WCYKnbiTalgFDvkQERITRmcfncZs6aVABedd5B85yQU5EULS4a5pnbpuI0Q==} + peerDependencies: + graphql: '*' + + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + graphql@16.12.0: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -3663,6 +4033,10 @@ packages: hamt-sharding@3.0.6: resolution: {integrity: sha512-nZeamxfymIWLpVcAN0CRrb7uVq3hCOGj9IcL6NMA6VVCVWqj+h9Jo/SmaWuS92AEDf1thmHsM5D5c70hM3j2Tg==} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3711,6 +4085,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-call@5.3.0: + resolution: {integrity: sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==} + engines: {node: '>=8.0.0'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3723,6 +4101,14 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.1: resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} engines: {node: '>=0.10.0'} @@ -3744,6 +4130,9 @@ packages: immer@11.1.3: resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3776,9 +4165,15 @@ packages: interface-blockstore@6.0.1: resolution: {integrity: sha512-AVcUbMwrhiO4RqDljUitUt3aoon6MD2fblsN7vEVBDsmHFQT0LIOODVK5Qxe28h1uUvVykyZqmo09f6w55KiJg==} + interface-datastore@8.3.2: + resolution: {integrity: sha512-R3NLts7pRbJKc3qFdQf+u40hK8XWc0w4Qkx3OFEstC80VoaDUABY/dXA2EJPhtNC+bsrf41Ehvqb6+pnIclyRA==} + interface-datastore@9.0.2: resolution: {integrity: sha512-jebn+GV/5LTDDoyicNIB4D9O0QszpPqT09Z/MpEWvf3RekjVKpXJCDguM5Au2fwIFxFDAQMZe5bSla0jMamCNg==} + interface-store@6.0.3: + resolution: {integrity: sha512-+WvfEZnFUhRwFxgz+QCQi7UC6o9AM0EHM9bpIe2Nhqb100NHCsTvNAn4eJgvgV2/tmLo1MP9nGxQKEcZTAueLA==} + interface-store@7.0.1: resolution: {integrity: sha512-OPRRUO3Cs6Jr/t98BrJLQp1jUTPgrRH0PqFfuNoPAqd+J7ABN1tjFVjQdaOBiybYJTS/AyBSZnZVWLPvp3dW3w==} @@ -3807,12 +4202,19 @@ packages: ipfs-unixfs-importer@16.1.4: resolution: {integrity: sha512-apnhvrTRFZMt7YUjyHJcvaPN1SSgBQ7OQIjTb2LppRDbY20kzVO8PcROLOzpinFmeZlVfx0QONy8aNfAcT3b2w==} + ipfs-unixfs@11.2.5: + resolution: {integrity: sha512-uasYJ0GLPbViaTFsOLnL9YPjX5VmhnqtWRriogAHOe4ApmIi9VAOFBzgDHsUW2ub4pEa/EysbtWk126g2vkU/g==} + ipfs-unixfs@12.0.1: resolution: {integrity: sha512-V8o80MEq3Aehs9KSX9k/FpRmsyYrLAH6mrq7Tq13vQi8TA3onzif8z5sDsWg8AAV9aa+uuvr0HHVLtnb5rNL3A==} ipns@10.1.3: resolution: {integrity: sha512-b2Zeh8+7qOV11NjnTsYLpG8K6T13uBMndpzk9N9E2Qjz/u80qsxvKpspSP32sErOLr/GWjdFVVc02E9PMojQNA==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -3825,6 +4227,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-electron@2.2.2: resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} @@ -3836,10 +4243,19 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -3851,6 +4267,9 @@ packages: is-loopback-addr@2.0.2: resolution: {integrity: sha512-26POf2KRCno/KTNL5Q0b/9TYnL00xEsSaLfiFRmjM7m7Lw7ZMmFybzzuX4CcsLAluZGd+niLUiMRxEooVE3aqg==} + is-natural-number@4.0.1: + resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==} + is-network-error@1.3.0: resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} engines: {node: '>=16'} @@ -3872,10 +4291,26 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-regexp@3.1.0: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} + is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -3888,6 +4323,13 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3897,9 +4339,18 @@ packages: iso-kv@3.1.1: resolution: {integrity: sha512-yKTLmUCc8gl0MXJs3ZaTaNDgfG/2ROasZERKPa5aYY7Ks/eb8BvGfLHrC+t1cHxiRkogvaXulDP77ovwLKgLPg==} + iso-url@1.2.1: + resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} + engines: {node: '>=12'} + iso-web@2.1.1: resolution: {integrity: sha512-P3qFt9hVgJx5lgUHY6TBoI575SHT7vt6BswXbcqd3BTZkBtEH59QxP6gMCtAACHxoWezbK2lTPj4yBoTBADDxQ==} + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + isows@1.0.7: resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: @@ -4011,6 +4462,9 @@ packages: it-to-buffer@4.0.10: resolution: {integrity: sha512-dXNHSILSPVv+31nxav+egNxWA/RpSuAHCSurJCLxkFDpmzAyYPJwIkPfLkYiHLoJqyE6Z5nVFILp6aDvz9V5pw==} + it-to-stream@1.0.0: + resolution: {integrity: sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==} + iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} @@ -4018,6 +4472,20 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jayson@4.2.0: + resolution: {integrity: sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==} + engines: {node: '>=8'} + hasBin: true + jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4076,6 +4544,10 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -4097,6 +4569,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -4112,6 +4587,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4123,6 +4601,9 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + kubo-rpc-client@5.4.1: + resolution: {integrity: sha512-v86bQWtyA//pXTrt9y4iEwjW6pt1gA18Z1famWXIR/HN5TFdYwQ3yHOlRE6JSWBDQ0rR6FOMyrrGy8To78mXow==} + kysely@0.28.9: resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} engines: {node: '>=20.0.0'} @@ -4217,6 +4698,10 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4236,18 +4721,70 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - lodash.throttle@4.1.1: - resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash.lowercase@4.3.0: + resolution: {integrity: sha512-UcvP1IZYyDKyEL64mmrwoA1AbFu5ahojhTtkOUr1K9dbuxzS9ev8i4TxMMGCqRC9TE8uDaSoufNAXxRPNTseVA==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + lodash.lowerfirst@4.3.1: + resolution: {integrity: sha512-UUKX7VhP1/JL54NXg2aq/E1Sfnjjes8fNYTNkPU8ZmsaVeBvPHKdbNaN79Re5XRL01u6wbq3j0cbYZj71Fcu5w==} + + lodash.pad@4.5.1: + resolution: {integrity: sha512-mvUHifnLqM+03YNzeTBS1/Gr6JRFjd3rRx88FHWUvamVaT9k2O/kXha3yBSOwB9/DTQrSTLJNHvLBBt2FdX7Mg==} + + lodash.padend@4.6.1: + resolution: {integrity: sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==} + + lodash.padstart@4.6.1: + resolution: {integrity: sha512-sW73O6S8+Tg66eY56DBk85aQzzUJDtpoXFBgELMd5P/SotAguo+1kYO6RuYgXxA4HJH3LFTFPASX6ET6bjfriw==} + + lodash.repeat@4.1.0: + resolution: {integrity: sha512-eWsgQW89IewS95ZOcr15HHCX6FVDxq3f2PNUIng3fyzsPev9imFQxIYdFZ6crl8L56UR6ZlGDLcEb3RZsCSSqw==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash.trim@4.18.0: + resolution: {integrity: sha512-q8B9MlXzN9NaTtS2JCd7kKl3RqwrVURgKEXoHDII8A/v7y3tWOq3rLEe+vN6LNvT+EYBVKVt6roNQxMkosS2aA==} + + lodash.trimend@4.18.0: + resolution: {integrity: sha512-8w2M3nZAWLN1OX/6mTPCwRlZiD/LhVyPV9l7DEbkd9wybExvg9AcCjbD19swj6oVzX5hcMZHp3/Y1b4Sl3sHKg==} + + lodash.trimstart@4.5.1: + resolution: {integrity: sha512-b/+D6La8tU76L/61/aN0jULWHkT0EeJCmVstPBn/K9MtD2qBW83AsBNrr63dKuWYwVMO7ucv13QNO/Ek/2RKaQ==} + + lodash.uppercase@4.3.0: + resolution: {integrity: sha512-+Nbnxkj7s8K5U8z6KnEYPGUOGp3woZbB7Ecs7v3LkkjLQSm2kP9SKIILitN1ktn2mB/tmM9oSlku06I+/lH7QA==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@3.0.0: + resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} + engines: {node: '>=8'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -4256,10 +4793,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - lru-cache@11.2.6: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} @@ -4267,6 +4800,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lucide-react@0.563.0: resolution: {integrity: sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==} peerDependencies: @@ -4292,6 +4829,10 @@ packages: main-event@1.0.1: resolution: {integrity: sha512-NWtdGrAca/69fm6DIVd8T9rtfDII4Q8NQbIbsKQq2VzS9eqOGYs8uaNQjcuaCq/d9H/o625aOTJX2Qoxzqw0Pw==} + make-dir@1.3.0: + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -4305,6 +4846,9 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + matchstick-as@0.6.0: + resolution: {integrity: sha512-E36fWsC1AbCkBFt05VsDDRoFvGSdcZg6oZJrtIe/YDBbuFh8SKbR5FcoqDhNWqSN+F7bN/iS2u8Md0SM+4pUpw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4450,17 +4994,21 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -4521,6 +5069,9 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true + multiformats@13.1.3: + resolution: {integrity: sha512-CZPi9lFZCM/+7oRolWYsvalsyWQGFo+GpdaTmjxXXomC+nP/W1Rnxb9sUgjvmNmRZ5bOPqRAl4nuK+Ydw/4tGw==} + multiformats@13.4.2: resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==} @@ -4545,6 +5096,11 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + native-fetch@4.0.2: + resolution: {integrity: sha512-4QcVlKFtv2EYVS5MBgsGX5+NWKtbDbIECdUXDBGDMAZXq3Jkv9zf+y8iS7Ub8fEdga3GpYeazp9gauNqXHJOCg==} + peerDependencies: + undici: '*' + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -4616,6 +5172,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -4653,10 +5213,18 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + ora@4.0.2: + resolution: {integrity: sha512-YUOZbamht5mfLxPmk4M35CD/5DuOkAacxlEUbStVXpBAt4fyhBf+vZHI/HRkI++QUp3sNoeA2Gw4C+hi4eGSig==} + engines: {node: '>=8'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -4672,6 +5240,10 @@ packages: typescript: optional: true + p-defer@3.0.0: + resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} + engines: {node: '>=8'} + p-defer@4.0.1: resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} engines: {node: '>=12'} @@ -4680,6 +5252,9 @@ packages: resolution: {integrity: sha512-nDq4JpyYnNPDrndgY3z9vQsB0X1bdsOcDmMFbLvewcnt38Geda9x+gULX3+RiGzwJ1QvzqTwNB4EQp+OwMOVAA==} engines: {node: '>=20'} + p-fifo@1.0.0: + resolution: {integrity: sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4731,6 +5306,13 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-duration@2.1.6: + resolution: {integrity: sha512-1/A2Exg3NcJGcYdgV/dn4frR7vO2hOW/ohQ4KIgbT4W3raVcpYSszPWiL6I6cKufi4jQM5NbGRXLBj8AoLM4iQ==} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4775,6 +5357,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pg-boss@12.11.1: resolution: {integrity: sha512-gb7zgSac6RwpA6LQvgwY/yJtYeHrwjX7ksCK1WJs5Hi3mHx4/1eFBD+UtAMcm9JIYTMlBwttsnV2GxsArguRAg==} engines: {node: '>=22.12.0'} @@ -4829,6 +5414,22 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -4880,6 +5481,11 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -4888,6 +5494,9 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} @@ -4901,6 +5510,10 @@ packages: progress-events@1.0.1: resolution: {integrity: sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prom-client@15.1.3: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} @@ -4908,6 +5521,9 @@ packages: promise@8.3.0: resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protons-runtime@5.6.0: resolution: {integrity: sha512-/Kde+sB9DsMFrddJT/UZWe6XqvL7SL5dbag/DBCElFKhkwDj7XKt53S+mzLyaDP5OqS0wXjV5SA572uWDaT0Hg==} @@ -4994,6 +5610,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-native-fetch-api@3.0.0: + resolution: {integrity: sha512-g2rtqPjdroaboDKTsJCTlcmtw54E25OjyaunUP0anOZn4Fuo2IKs8BVfe02zVggA/UysbmfSnRJIqtNkAgggNA==} + react-native-webrtc@124.0.7: resolution: {integrity: sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==} peerDependencies: @@ -5051,6 +5670,9 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -5093,6 +5715,10 @@ packages: regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} + engines: {node: '>=14'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -5137,6 +5763,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -5151,6 +5782,10 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5160,9 +5795,16 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safe-regex2@5.0.0: resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} @@ -5198,10 +5840,19 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + seek-bzip@1.0.6: + resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.3.5: + resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -5370,6 +6021,15 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + stream-to-it@1.0.1: + resolution: {integrity: sha512-AqHYAYPHcmvMrcLNgncE/q0Aj/ajP6A4qGhxP6EVn7K3YTNs0bJpJyk57wc2Heb7MUL64jurvmnmui8D9kjZgA==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5385,9 +6045,16 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5400,6 +6067,13 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-dirs@2.1.0: + resolution: {integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -5434,6 +6108,10 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5472,6 +6150,10 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-stream@1.6.2: + resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} + engines: {node: '>= 0.8.0'} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -5511,6 +6193,9 @@ packages: throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} @@ -5547,6 +6232,13 @@ packages: resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} hasBin: true + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -5630,6 +6322,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + type-fest@0.7.1: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} @@ -5730,9 +6426,16 @@ packages: uint8arrays@5.1.0: resolution: {integrity: sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==} + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.16.0: + resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + engines: {node: '>=20.18.1'} + undici@7.21.0: resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} @@ -5770,6 +6473,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -5781,6 +6487,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -5789,6 +6498,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -5895,6 +6608,10 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + wabt@1.0.24: + resolution: {integrity: sha512-8l7sIOd3i5GWfTWciPL0+ff/FK/deVK2Q6FN+MPz4vfUcD78i2M/49XJTwF6aml91uIiuXJEsLKWMB2cw/mtKg==} + hasBin: true + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -5908,6 +6625,26 @@ packages: weald@1.1.1: resolution: {integrity: sha512-PaEQShzMCz8J/AD2N3dJMc1hTZWkJeLKS2NMeiVkV5KDHwgZe7qXLEzyodsT/SODxWDdXJJqocuwf3kHzcXhSQ==} + web3-errors@1.3.1: + resolution: {integrity: sha512-w3NMJujH+ZSW4ltIZZKtdbkbyQEvBzyp3JRn59Ckli0Nz4VMsVq8aF1bLWM7A2kuQ+yVEm3ySeNU+7mSRwx7RQ==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-eth-abi@4.4.1: + resolution: {integrity: sha512-60ecEkF6kQ9zAfbTY04Nc9q4eEYM0++BySpGi8wZ2PD1tw/c0SDvsKhV6IKURxLJhsDlb08dATc3iD6IbtWJmg==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-types@1.10.0: + resolution: {integrity: sha512-0IXoaAFtFc8Yin7cCdQfB9ZmjafrbP6BO0f0KT/khMhXKUpoJ6yShrVhiNpyRBo8QQjuOagsWzwSK2H49I7sbw==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-utils@4.3.3: + resolution: {integrity: sha512-kZUeCwaQm+RNc2Bf1V3BYbF29lQQKz28L0y+FA4G0lS8IxtJVGi5SeDTUkpwqqkdHHC7JcapPDnyyzJ1lfWlOw==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-validator@2.0.6: + resolution: {integrity: sha512-qn9id0/l1bWmvH4XfnG/JtGKKwut2Vokl6YXP5Kfg424npysmtRLe9DgiNBM9Op7QL/aSiaA0TVXibuIuWcizg==} + engines: {node: '>=14', npm: '>=6.12.0'} + webcrypto-core@1.8.1: resolution: {integrity: sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==} @@ -5974,6 +6711,13 @@ packages: engines: {node: '>=8'} hasBin: true + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6029,6 +6773,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -6055,6 +6803,18 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -6068,6 +6828,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -6080,9 +6843,33 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} +catalogs: + default: + '@biomejs/biome': + specifier: 2.3.14 + version: 2.3.14 + '@swc/core': + specifier: 1.15.11 + version: 1.15.11 + '@vitest/coverage-v8': + specifier: 4.0.18 + version: 4.0.18 + typescript: + specifier: 5.9.3 + version: 5.9.3 + unplugin-swc: + specifier: 1.5.9 + version: 1.5.9 + vitest: + specifier: 4.0.18 + version: 4.0.18 + snapshots: '@acemir/cssom@0.9.31': {} @@ -6209,7 +6996,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6368,7 +7155,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -6577,6 +7364,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.1.0 + '@fastify/busboy@3.2.0': {} + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': @@ -6626,6 +7415,55 @@ snapshots: transitivePeerDependencies: - typescript + '@float-capital/float-subgraph-uncrashable@0.0.0-internal-testing.5': + dependencies: + '@rescript/std': 9.0.0 + graphql: 16.12.0 + graphql-import-node: 0.0.5(graphql@16.12.0) + js-yaml: 4.1.1 + + '@graphprotocol/graph-cli@0.98.1(@types/node@25.2.3)(typescript@5.9.3)(zod@3.25.76)': + dependencies: + '@float-capital/float-subgraph-uncrashable': 0.0.0-internal-testing.5 + '@oclif/core': 4.5.5 + '@oclif/plugin-autocomplete': 3.2.45 + '@oclif/plugin-not-found': 3.2.80(@types/node@25.2.3) + '@oclif/plugin-warn-if-update-available': 3.1.60 + '@pinax/graph-networks-registry': 0.7.1 + '@whatwg-node/fetch': 0.10.13 + assemblyscript: 0.19.23 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + decompress: 4.2.1 + docker-compose: 1.3.0 + fs-extra: 11.3.2 + glob: 11.0.3 + gluegun: 5.2.0(debug@4.4.3) + graphql: 16.11.0 + immutable: 5.1.4 + jayson: 4.2.0 + js-yaml: 4.1.0 + kubo-rpc-client: 5.4.1(undici@7.16.0) + open: 10.2.0 + prettier: 3.6.2 + progress: 2.0.3 + semver: 7.7.3 + tmp-promise: 3.0.3 + undici: 7.16.0 + web3-eth-abi: 4.4.1(typescript@5.9.3)(zod@3.25.76) + yaml: 2.8.1 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - supports-color + - typescript + - utf-8-validate + - zod + + '@graphprotocol/graph-ts@0.38.2': + dependencies: + assemblyscript: 0.27.31 + '@hapi/address@5.1.1': dependencies: '@hapi/hoek': 11.0.7 @@ -6725,7 +7563,7 @@ snapshots: multiformats: 13.4.2 uint8arrays: 5.1.0 - '@helia/unixfs@7.1.0': + '@helia/unixfs@7.1.0(encoding@0.1.13)': dependencies: '@helia/interface': 6.1.1 '@ipld/dag-pb': 4.1.5 @@ -6736,7 +7574,7 @@ snapshots: interface-blockstore: 6.0.1 ipfs-unixfs: 12.0.1 ipfs-unixfs-exporter: 15.0.3 - ipfs-unixfs-importer: 16.1.4 + ipfs-unixfs-importer: 16.1.4(encoding@0.1.13) it-all: 3.0.9 it-first: 3.0.9 it-glob: 3.0.4 @@ -6963,12 +7801,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -6978,6 +7810,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/cliui@9.0.0': {} + '@isaacs/ttlcache@1.4.1': {} '@istanbuljs/load-nyc-config@1.1.0': @@ -7227,6 +8061,17 @@ snapshots: '@multiformats/multiaddr': 13.0.1 progress-events: 1.0.1 + '@libp2p/interface@2.11.0': + dependencies: + '@multiformats/dns': 1.0.11 + '@multiformats/multiaddr': 12.5.1 + it-pushable: 3.2.3 + it-stream-types: 2.0.2 + main-event: 1.0.1 + multiformats: 13.4.2 + progress-events: 1.0.1 + uint8arraylist: 2.4.8 + '@libp2p/interface@3.1.0': dependencies: '@multiformats/dns': 1.0.11 @@ -7281,6 +8126,14 @@ snapshots: sanitize-filename: 1.6.3 uint8arrays: 5.1.0 + '@libp2p/logger@5.2.0': + dependencies: + '@libp2p/interface': 2.11.0 + '@multiformats/multiaddr': 12.5.1 + interface-datastore: 8.3.2 + multiformats: 13.4.2 + weald: 1.1.1 + '@libp2p/logger@6.2.2': dependencies: '@libp2p/interface': 3.1.0 @@ -7325,6 +8178,13 @@ snapshots: '@libp2p/utils': 7.0.13 multiformats: 13.4.2 + '@libp2p/peer-id@5.1.9': + dependencies: + '@libp2p/crypto': 5.1.13 + '@libp2p/interface': 2.11.0 + multiformats: 13.4.2 + uint8arrays: 5.1.0 + '@libp2p/peer-id@6.0.4': dependencies: '@libp2p/crypto': 5.1.13 @@ -7527,10 +8387,24 @@ snapshots: dependencies: '@multiformats/multiaddr': 13.0.1 + '@multiformats/multiaddr-to-uri@11.0.2': + dependencies: + '@multiformats/multiaddr': 12.5.1 + '@multiformats/multiaddr-to-uri@12.0.0': dependencies: '@multiformats/multiaddr': 13.0.1 + '@multiformats/multiaddr@12.5.1': + dependencies: + '@chainsafe/is-ip': 2.1.0 + '@chainsafe/netmask': 2.0.0 + '@multiformats/dns': 1.0.11 + abort-error: 1.0.1 + multiformats: 13.4.2 + uint8-varint: 2.0.4 + uint8arrays: 5.1.0 + '@multiformats/multiaddr@13.0.1': dependencies: '@chainsafe/is-ip': 2.1.0 @@ -7639,7 +8513,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/schedule@6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': + '@nestjs/schedule@6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7656,7 +8530,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7671,7 +8545,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/testing@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13)': + '@nestjs/testing@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13))': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7679,7 +8553,7 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.18.0)(ts-node@10.9.2(@swc/core@1.15.11)(@types/node@25.2.3)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.18.0)(ts-node@10.9.2(@swc/core@1.15.11)(@types/node@25.2.3)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -7691,6 +8565,10 @@ snapshots: '@noble/ciphers@2.1.1': {} + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + '@noble/curves@1.9.1': dependencies: '@noble/hashes': 1.8.0 @@ -7703,6 +8581,8 @@ snapshots: dependencies: '@noble/hashes': 2.0.1 + '@noble/hashes@1.4.0': {} + '@noble/hashes@1.8.0': {} '@noble/hashes@2.0.1': {} @@ -7723,6 +8603,77 @@ snapshots: dependencies: consola: 3.4.2 + '@oclif/core@4.10.5': + dependencies: + ansi-escapes: 4.3.2 + ansis: 3.17.0 + clean-stack: 3.0.1 + cli-spinners: 2.9.2 + debug: 4.4.3(supports-color@8.1.1) + ejs: 3.1.10 + get-package-type: 0.1.0 + indent-string: 4.0.0 + is-wsl: 2.2.0 + lilconfig: 3.1.3 + minimatch: 10.2.5 + semver: 7.7.4 + string-width: 4.2.3 + supports-color: 8.1.1 + tinyglobby: 0.2.15 + widest-line: 3.1.0 + wordwrap: 1.0.0 + wrap-ansi: 7.0.0 + + '@oclif/core@4.5.5': + dependencies: + ansi-escapes: 4.3.2 + ansis: 3.17.0 + clean-stack: 3.0.1 + cli-spinners: 2.9.2 + debug: 4.4.3(supports-color@8.1.1) + ejs: 3.1.10 + get-package-type: 0.1.0 + indent-string: 4.0.0 + is-wsl: 2.2.0 + lilconfig: 3.1.3 + minimatch: 9.0.5 + semver: 7.7.4 + string-width: 4.2.3 + supports-color: 8.1.1 + tinyglobby: 0.2.15 + widest-line: 3.1.0 + wordwrap: 1.0.0 + wrap-ansi: 7.0.0 + + '@oclif/plugin-autocomplete@3.2.45': + dependencies: + '@oclif/core': 4.10.5 + ansis: 3.17.0 + debug: 4.4.3(supports-color@8.1.1) + ejs: 3.1.10 + transitivePeerDependencies: + - supports-color + + '@oclif/plugin-not-found@3.2.80(@types/node@25.2.3)': + dependencies: + '@inquirer/prompts': 7.10.1(@types/node@25.2.3) + '@oclif/core': 4.10.5 + ansis: 3.17.0 + fast-levenshtein: 3.0.0 + transitivePeerDependencies: + - '@types/node' + + '@oclif/plugin-warn-if-update-available@3.1.60': + dependencies: + '@oclif/core': 4.10.5 + ansis: 3.17.0 + debug: 4.4.3(supports-color@8.1.1) + http-call: 5.3.0 + lodash: 4.18.1 + registry-auth-token: 5.1.1 + transitivePeerDependencies: + - supports-color + '@open-draft/deferred-promise@2.2.0': {} '@open-draft/logger@0.3.0': @@ -8101,11 +9052,25 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 + '@pinax/graph-networks-registry@0.7.1': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': optional: true + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@3.0.2': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + '@prisma/instrumentation@7.4.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8214,7 +9179,7 @@ snapshots: '@react-native/community-cli-plugin@0.83.1': dependencies: '@react-native/dev-middleware': 0.83.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) invariant: 2.2.4 metro: 0.83.3 metro-config: 0.83.3 @@ -8240,7 +9205,7 @@ snapshots: chrome-launcher: 0.15.2 chromium-edge-launcher: 0.2.0 connect: 3.7.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 @@ -8278,6 +9243,8 @@ snapshots: react: 19.2.4 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + '@rescript/std@9.0.0': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/pluginutils@5.3.0(rollup@4.53.4)': @@ -8356,14 +9323,27 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@scure/base@1.1.9': {} + '@scure/base@1.2.6': {} + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + '@scure/bip32@1.7.0': dependencies: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + '@scure/bip39@1.6.0': dependencies: '@noble/hashes': 1.8.0 @@ -8608,7 +9588,7 @@ snapshots: '@tokenizer/inflate@0.4.1': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) token-types: 6.1.1 transitivePeerDependencies: - supports-color @@ -8748,10 +9728,14 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/node@12.20.55': {} + '@types/node@25.2.3': dependencies: undici-types: 7.16.0 + '@types/parse-json@4.0.2': {} + '@types/pg-pool@2.0.7': dependencies: '@types/pg': 8.15.6 @@ -8813,6 +9797,10 @@ snapshots: '@types/validator@13.15.10': {} + '@types/ws@7.4.7': + dependencies: + '@types/node': 25.2.3 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -8967,6 +9955,27 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@whatwg-node/disposablestack@0.0.6': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/fetch@0.10.13': + dependencies: + '@whatwg-node/node-fetch': 0.8.5 + urlpattern-polyfill: 10.1.0 + + '@whatwg-node/node-fetch@0.8.5': + dependencies: + '@fastify/busboy': 3.2.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/promise-helpers@1.3.2': + dependencies: + tslib: 2.8.1 + '@willsoto/nestjs-prometheus@6.0.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3)': dependencies: '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -8976,6 +9985,12 @@ snapshots: '@xtuc/long@4.2.2': {} + abitype@0.7.1(typescript@5.9.3)(zod@3.25.76): + dependencies: + typescript: 5.9.3 + optionalDependencies: + zod: 3.25.76 + abitype@1.2.3(typescript@5.9.3)(zod@4.3.6): optionalDependencies: typescript: 5.9.3 @@ -9004,7 +10019,7 @@ snapshots: '@peculiar/x509': 1.14.3 asn1js: 3.0.7 axios: 1.13.5(debug@4.4.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) node-forge: 1.3.3 transitivePeerDependencies: - supports-color @@ -9060,10 +10075,20 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -9072,6 +10097,8 @@ snapshots: ansi-styles@6.2.3: {} + ansis@3.17.0: {} + ansis@4.2.0: {} any-signal@4.2.0: {} @@ -9081,6 +10108,14 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + apisauce@2.1.6(debug@4.4.3): + dependencies: + axios: 0.21.4(debug@4.4.3) + transitivePeerDependencies: + - debug + + app-module-path@2.2.0: {} + app-root-path@3.1.0: {} append-field@1.0.0: {} @@ -9107,6 +10142,17 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + assemblyscript@0.19.23: + dependencies: + binaryen: 102.0.0-nightly.20211028 + long: 5.3.2 + source-map-support: 0.5.21 + + assemblyscript@0.27.31: + dependencies: + binaryen: 116.0.0-nightly.20240114 + long: 5.3.2 + assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.11: @@ -9115,6 +10161,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + async@3.2.6: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -9133,6 +10181,12 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.20.1 + axios@0.21.4(debug@4.4.3): + dependencies: + follow-redirects: 1.15.11(debug@4.4.3) + transitivePeerDependencies: + - debug + axios@1.13.5(debug@4.4.3): dependencies: follow-redirects: 1.15.11(debug@4.4.3) @@ -9212,8 +10266,17 @@ snapshots: dependencies: require-from-string: 2.0.2 + binaryen@102.0.0-nightly.20211028: {} + + binaryen@116.0.0-nightly.20240114: {} + bintrees@1.0.2: {} + bl@1.2.3: + dependencies: + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -9226,6 +10289,10 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blob-to-it@2.0.12: + dependencies: + browser-readablestream-to-it: 2.0.10 + blockstore-core@6.1.2: dependencies: '@libp2p/logger': 6.2.2 @@ -9240,7 +10307,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.1 on-finished: 2.4.1 @@ -9263,6 +10330,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -9281,6 +10352,17 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-alloc-unsafe@1.1.0: {} + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + + buffer-crc32@0.2.13: {} + + buffer-fill@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -9293,6 +10375,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -9328,6 +10414,12 @@ snapshots: chai@6.2.1: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -9381,12 +10473,23 @@ snapshots: dependencies: clsx: 2.1.1 + clean-stack@3.0.1: + dependencies: + escape-string-regexp: 4.0.0 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 cli-spinners@2.9.2: {} + cli-table3@0.6.0: + dependencies: + object-assign: 4.1.1 + string-width: 4.2.3 + optionalDependencies: + colors: 1.4.0 + cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -9409,12 +10512,20 @@ snapshots: clsx@2.1.1: {} + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} + colors@1.4.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -9456,6 +10567,11 @@ snapshots: semver: 7.7.4 uint8array-extras: 1.5.0 + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + connect@3.7.0: dependencies: debug: 2.6.9 @@ -9490,6 +10606,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig@7.0.1: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -9510,6 +10634,12 @@ snapshots: '@types/luxon': 3.7.1 luxon: 3.7.2 + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -9570,6 +10700,11 @@ snapshots: d3-timer@3.0.1: {} + dag-jose@5.1.1: + dependencies: + '@ipld/dag-cbor': 9.2.5 + multiformats: 13.1.3 + data-urls@7.0.0(@noble/hashes@2.0.1): dependencies: whatwg-mimetype: 5.0.0 @@ -9608,9 +10743,11 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decimal.js-light@2.5.1: {} @@ -9620,12 +10757,57 @@ snapshots: dependencies: mimic-response: 3.1.0 + decompress-tar@4.1.1: + dependencies: + file-type: 5.2.0 + is-stream: 1.1.0 + tar-stream: 1.6.2 + + decompress-tarbz2@4.1.1: + dependencies: + decompress-tar: 4.1.1 + file-type: 6.2.0 + is-stream: 1.1.0 + seek-bzip: 1.0.6 + unbzip2-stream: 1.4.3 + + decompress-targz@4.1.1: + dependencies: + decompress-tar: 4.1.1 + file-type: 5.2.0 + is-stream: 1.1.0 + + decompress-unzip@4.0.1: + dependencies: + file-type: 3.9.0 + get-stream: 2.3.1 + pify: 2.3.0 + yauzl: 2.10.0 + + decompress@4.2.1: + dependencies: + decompress-tar: 4.1.1 + decompress-tarbz2: 4.1.1 + decompress-targz: 4.1.1 + decompress-unzip: 4.0.1 + graceful-fs: 4.2.11 + make-dir: 1.3.0 + pify: 2.3.0 + strip-dirs: 2.1.0 + dedent@1.7.0: {} deep-extend@0.6.0: {} deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -9636,6 +10818,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + + delay@5.0.0: {} + delay@6.0.0: {} delay@7.0.0: @@ -9670,6 +10856,10 @@ snapshots: dependencies: from-exponential: 1.1.1 + docker-compose@1.3.0: + dependencies: + yaml: 2.8.2 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -9696,6 +10886,18 @@ snapshots: ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + ejs@3.1.8: + dependencies: + jake: 10.9.4 + + electron-fetch@1.9.1: + dependencies: + encoding: 0.1.13 + electron-to-chromium@1.5.267: {} emoji-regex@8.0.0: {} @@ -9706,6 +10908,10 @@ snapshots: encodeurl@2.0.0: {} + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -9715,6 +10921,10 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + entities@6.0.1: {} env-paths@3.0.0: {} @@ -9750,6 +10960,12 @@ snapshots: es-toolkit@1.44.0: {} + es6-promise@4.2.8: {} + + es6-promisify@5.0.0: + dependencies: + es6-promise: 4.2.8 + esbuild@0.27.1: optionalDependencies: '@esbuild/aix-ppc64': 0.27.1 @@ -9783,6 +10999,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -9810,6 +11028,13 @@ snapshots: etag@1.8.1: {} + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + event-target-shim@5.0.1: {} event-target-shim@6.0.2: {} @@ -9820,6 +11045,18 @@ snapshots: events@3.3.0: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -9834,7 +11071,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -9859,10 +11096,14 @@ snapshots: transitivePeerDependencies: - supports-color + eyes@0.1.8: {} + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9882,6 +11123,10 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-levenshtein@3.0.0: + dependencies: + fastest-levenshtein: 1.0.16 + fast-querystring@1.1.2: dependencies: fast-decode-uri-component: 1.0.1 @@ -9890,6 +11135,8 @@ snapshots: fast-uri@3.1.0: {} + fastest-levenshtein@1.0.16: {} + fastify@5.8.2: dependencies: '@fastify/ajv-compiler': 4.0.5 @@ -9918,6 +11165,10 @@ snapshots: dependencies: bser: 2.1.1 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -9931,7 +11182,13 @@ snapshots: transitivePeerDependencies: - supports-color - filecoin-pin@0.20.0(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3)(zod@4.3.6): + file-type@3.9.0: {} + + file-type@5.2.0: {} + + file-type@6.2.0: {} + + filecoin-pin@0.20.0(encoding@0.1.13)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3)(zod@4.3.6): dependencies: '@chainsafe/libp2p-noise': 17.0.0 '@chainsafe/libp2p-yamux': 8.0.1 @@ -9939,7 +11196,7 @@ snapshots: '@filoz/synapse-core': 0.3.3(typescript@5.9.3)(viem@2.47.5(typescript@5.9.3)(zod@4.3.6)) '@filoz/synapse-sdk': 0.40.2(typescript@5.9.3)(viem@2.47.5(typescript@5.9.3)(zod@4.3.6)) '@helia/block-brokers': 5.1.4 - '@helia/unixfs': 7.1.0 + '@helia/unixfs': 7.1.0(encoding@0.1.13) '@ipld/car': 5.4.2 '@libp2p/identify': 4.0.13 '@libp2p/tcp': 11.0.13 @@ -9970,6 +11227,10 @@ snapshots: - utf-8-validate - zod + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -9988,7 +11249,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -10012,7 +11273,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -10035,7 +11296,7 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.3 + semver: 7.7.4 tapable: 2.3.0 typescript: 5.9.3 webpack: 5.104.1(@swc/core@1.15.11) @@ -10074,6 +11335,17 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-jetpack@4.3.1: + dependencies: + minimatch: 3.1.2 + rimraf: 2.7.1 + fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -10085,6 +11357,8 @@ snapshots: function-timeout@0.1.1: {} + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -10102,6 +11376,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-iterator@1.0.2: {} + get-iterator@2.0.1: {} get-package-type@0.1.0: {} @@ -10113,6 +11389,13 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@2.3.1: + dependencies: + object-assign: 4.1.1 + pinkie-promise: 2.0.1 + + get-stream@6.0.1: {} + github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -10130,9 +11413,18 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.1 + glob@13.0.0: dependencies: - minimatch: 10.1.1 + minimatch: 10.2.4 minipass: 7.1.2 path-scurry: 2.0.1 @@ -10145,10 +11437,53 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + gluegun@5.2.0(debug@4.4.3): + dependencies: + apisauce: 2.1.6(debug@4.4.3) + app-module-path: 2.2.0 + cli-table3: 0.6.0 + colors: 1.4.0 + cosmiconfig: 7.0.1 + cross-spawn: 7.0.3 + ejs: 3.1.8 + enquirer: 2.3.6 + execa: 5.1.1 + fs-jetpack: 4.3.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.lowercase: 4.3.0 + lodash.lowerfirst: 4.3.1 + lodash.pad: 4.5.1 + lodash.padend: 4.6.1 + lodash.padstart: 4.6.1 + lodash.repeat: 4.1.0 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.trim: 4.18.0 + lodash.trimend: 4.18.0 + lodash.trimstart: 4.5.1 + lodash.uppercase: 4.3.0 + lodash.upperfirst: 4.3.1 + ora: 4.0.2 + pluralize: 8.0.0 + semver: 7.3.5 + which: 2.0.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - debug + gopd@1.2.0: {} + graceful-fs@4.2.10: {} + graceful-fs@4.2.11: {} + graphql-import-node@0.0.5(graphql@16.12.0): + dependencies: + graphql: 16.12.0 + + graphql@16.11.0: {} + graphql@16.12.0: {} hamt-sharding@3.0.6: @@ -10156,6 +11491,8 @@ snapshots: sparse-array: 1.3.2 uint8arrays: 5.1.0 + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -10235,6 +11572,17 @@ snapshots: html-escaper@2.0.2: {} + http-call@5.3.0: + dependencies: + content-type: 1.0.5 + debug: 4.4.3(supports-color@8.1.1) + is-retry-allowed: 1.2.0 + is-stream: 2.0.1 + parse-json: 4.0.0 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - supports-color + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -10246,17 +11594,23 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color + human-signals@2.1.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.1: dependencies: safer-buffer: 2.1.2 @@ -10273,6 +11627,8 @@ snapshots: immer@11.1.3: {} + immutable@5.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -10310,11 +11666,18 @@ snapshots: interface-store: 7.0.1 multiformats: 13.4.2 + interface-datastore@8.3.2: + dependencies: + interface-store: 6.0.3 + uint8arrays: 5.1.0 + interface-datastore@9.0.2: dependencies: interface-store: 7.0.1 uint8arrays: 5.1.0 + interface-store@6.0.3: {} + interface-store@7.0.1: {} internmap@2.0.3: {} @@ -10348,7 +11711,7 @@ snapshots: p-queue: 9.1.0 progress-events: 1.0.1 - ipfs-unixfs-importer@16.1.4: + ipfs-unixfs-importer@16.1.4(encoding@0.1.13): dependencies: '@ipld/dag-pb': 4.1.5 '@multiformats/murmur3': 2.1.8 @@ -10363,13 +11726,18 @@ snapshots: it-parallel-batch: 3.0.9 multiformats: 13.4.2 progress-events: 1.0.1 - rabin-wasm: 0.1.5 + rabin-wasm: 0.1.5(encoding@0.1.13) uint8arraylist: 2.4.8 uint8arrays: 5.1.0 transitivePeerDependencies: - encoding - supports-color + ipfs-unixfs@11.2.5: + dependencies: + protons-runtime: 5.6.0 + uint8arraylist: 2.4.8 + ipfs-unixfs@12.0.1: dependencies: protons-runtime: 5.6.0 @@ -10388,22 +11756,41 @@ snapshots: uint8arraylist: 2.4.8 uint8arrays: 5.1.0 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-arrayish@0.2.1: {} is-callable@1.2.7: {} is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-electron@2.2.2: {} is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@1.0.0: {} is-ip@5.0.1: @@ -10413,6 +11800,8 @@ snapshots: is-loopback-addr@2.0.2: {} + is-natural-number@4.0.1: {} + is-network-error@1.3.0: {} is-node-process@1.2.0: {} @@ -10425,8 +11814,21 @@ snapshots: is-promise@4.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-regexp@3.1.0: {} + is-retry-allowed@1.2.0: {} + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.19 @@ -10437,6 +11839,12 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -10447,12 +11855,18 @@ snapshots: idb-keyval: 6.2.2 kysely: 0.28.9 + iso-url@1.2.1: {} + iso-web@2.1.1: dependencies: delay: 7.0.0 iso-kv: 3.1.1 p-retry: 7.1.1 + isomorphic-ws@4.0.1(ws@7.5.10): + dependencies: + ws: 7.5.10 + isows@1.0.7(ws@8.18.3): dependencies: ws: 8.18.3 @@ -10602,6 +12016,15 @@ snapshots: dependencies: uint8arrays: 5.1.0 + it-to-stream@1.0.0: + dependencies: + buffer: 6.0.3 + fast-fifo: 1.3.2 + get-iterator: 1.0.2 + p-defer: 3.0.0 + p-fifo: 1.0.0 + readable-stream: 3.6.2 + iterare@1.2.1: {} jackspeak@3.4.3: @@ -10610,6 +12033,34 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + jayson@4.2.0: + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.10) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -10709,6 +12160,10 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -10743,6 +12198,8 @@ snapshots: jsesc@3.1.0: {} + json-parse-better-errors@1.0.2: {} + json-parse-even-better-errors@2.3.1: {} json-schema-ref-resolver@3.0.0: @@ -10755,6 +12212,8 @@ snapshots: json-schema-typed@8.0.2: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -10765,6 +12224,44 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + kubo-rpc-client@5.4.1(undici@7.16.0): + dependencies: + '@ipld/dag-cbor': 9.2.5 + '@ipld/dag-json': 10.2.5 + '@ipld/dag-pb': 4.1.5 + '@libp2p/crypto': 5.1.13 + '@libp2p/interface': 2.11.0 + '@libp2p/logger': 5.2.0 + '@libp2p/peer-id': 5.1.9 + '@multiformats/multiaddr': 12.5.1 + '@multiformats/multiaddr-to-uri': 11.0.2 + any-signal: 4.2.0 + blob-to-it: 2.0.12 + browser-readablestream-to-it: 2.0.10 + dag-jose: 5.1.1 + electron-fetch: 1.9.1 + err-code: 3.0.1 + ipfs-unixfs: 11.2.5 + iso-url: 1.2.1 + it-all: 3.0.9 + it-first: 3.0.9 + it-glob: 3.0.4 + it-last: 3.0.9 + it-map: 3.1.4 + it-peekable: 3.0.8 + it-to-stream: 1.0.0 + merge-options: 3.0.4 + multiformats: 13.4.2 + nanoid: 5.1.6 + native-fetch: 4.0.2(undici@7.16.0) + parse-duration: 2.1.6 + react-native-fetch-api: 3.0.0 + stream-to-it: 1.0.1 + uint8arrays: 5.1.0 + wherearewe: 2.0.1 + transitivePeerDependencies: + - undici + kysely@0.28.9: {} leven@3.1.0: {} @@ -10863,6 +12360,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} load-esm@1.0.3: {} @@ -10875,31 +12374,69 @@ snapshots: dependencies: p-locate: 4.1.0 + lodash.camelcase@4.3.0: {} + + lodash.kebabcase@4.1.1: {} + + lodash.lowercase@4.3.0: {} + + lodash.lowerfirst@4.3.1: {} + + lodash.pad@4.5.1: {} + + lodash.padend@4.6.1: {} + + lodash.padstart@4.6.1: {} + + lodash.repeat@4.1.0: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + lodash.throttle@4.1.1: {} - lodash@4.17.21: {} + lodash.trim@4.18.0: {} + + lodash.trimend@4.18.0: {} + + lodash.trimstart@4.5.1: {} + + lodash.uppercase@4.3.0: {} + + lodash.upperfirst@4.3.1: {} lodash@4.17.23: {} + lodash@4.18.1: {} + + log-symbols@3.0.0: + dependencies: + chalk: 2.4.2 + log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 lru-cache@10.4.3: {} - lru-cache@11.2.4: {} - lru-cache@11.2.6: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lucide-react@0.563.0(react@19.2.4): dependencies: react: 19.2.4 @@ -10924,9 +12461,13 @@ snapshots: main-event@1.0.1: {} + make-dir@1.3.0: + dependencies: + pify: 3.0.0 + make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-error@1.3.6: {} @@ -10936,6 +12477,10 @@ snapshots: marky@1.3.0: {} + matchstick-as@0.6.0: + dependencies: + wabt: 1.0.24 + math-intrinsics@1.1.0: {} mdn-data@2.12.2: {} @@ -11007,7 +12552,7 @@ snapshots: metro-file-map@0.83.3: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -11103,7 +12648,7 @@ snapshots: chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -11166,18 +12711,22 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -11250,6 +12799,8 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 + multiformats@13.1.3: {} + multiformats@13.4.2: {} murmurhash3js-revisited@3.0.0: {} @@ -11262,6 +12813,10 @@ snapshots: napi-build-utils@2.0.0: {} + native-fetch@4.0.2(undici@7.16.0): + dependencies: + undici: 7.16.0 + negotiator@0.6.3: {} negotiator@1.0.0: {} @@ -11294,11 +12849,13 @@ snapshots: node-emoji@1.11.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-forge@1.3.3: {} @@ -11310,6 +12867,10 @@ snapshots: normalize-path@3.0.0: {} + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + nullthrows@1.1.1: {} ob1@0.83.3: @@ -11340,11 +12901,28 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + open@7.4.2: dependencies: is-docker: 2.2.1 is-wsl: 2.2.0 + ora@4.0.2: + dependencies: + chalk: 2.4.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + log-symbols: 3.0.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -11374,12 +12952,19 @@ snapshots: transitivePeerDependencies: - zod + p-defer@3.0.0: {} + p-defer@4.0.1: {} p-event@7.0.2: dependencies: p-timeout: 6.1.4 + p-fifo@1.0.0: + dependencies: + fast-fifo: 1.3.2 + p-defer: 3.0.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -11421,6 +13006,13 @@ snapshots: dependencies: callsites: 3.1.0 + parse-duration@2.1.6: {} + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -11447,7 +13039,7 @@ snapshots: path-scurry@2.0.1: dependencies: - lru-cache: 11.2.4 + lru-cache: 11.2.6 minipass: 7.1.2 path-to-regexp@6.3.0: {} @@ -11458,6 +13050,8 @@ snapshots: pathe@2.0.3: {} + pend@1.2.0: {} + pg-boss@12.11.1: dependencies: cron-parser: 5.5.0 @@ -11509,6 +13103,16 @@ snapshots: picomatch@4.0.3: {} + pify@2.3.0: {} + + pify@3.0.0: {} + + pinkie-promise@2.0.1: + dependencies: + pinkie: 2.0.4 + + pinkie@2.0.4: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -11573,6 +13177,8 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 + prettier@3.6.2: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -11585,6 +13191,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-nextick-args@2.0.1: {} + process-warning@4.0.1: {} process-warning@5.0.0: {} @@ -11593,6 +13201,8 @@ snapshots: progress-events@1.0.1: {} + progress@2.0.3: {} + prom-client@15.1.3: dependencies: '@opentelemetry/api': 1.9.0 @@ -11602,6 +13212,8 @@ snapshots: dependencies: asap: 2.0.6 + proto-list@1.2.4: {} + protons-runtime@5.6.0: dependencies: uint8-varint: 2.0.4 @@ -11640,13 +13252,13 @@ snapshots: quick-format-unescaped@4.0.4: {} - rabin-wasm@0.1.5: + rabin-wasm@0.1.5(encoding@0.1.13): dependencies: '@assemblyscript/loader': 0.9.4 bl: 5.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimist: 1.2.8 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) readable-stream: 3.6.2 transitivePeerDependencies: - encoding @@ -11704,6 +13316,10 @@ snapshots: react-is@18.3.1: {} + react-native-fetch-api@3.0.0: + dependencies: + p-defer: 3.0.0 + react-native-webrtc@124.0.7(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.4)): dependencies: base64-js: 1.5.1 @@ -11790,6 +13406,16 @@ snapshots: react@19.2.4: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -11843,13 +13469,17 @@ snapshots: regenerator-runtime@0.13.11: {} + registry-auth-token@5.1.1: + dependencies: + '@pnpm/npm-conf': 3.0.2 + require-directory@2.1.1: {} require-from-string@2.0.2: {} require-in-the-middle@8.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color @@ -11875,6 +13505,10 @@ snapshots: rfdc@1.4.1: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -11909,7 +13543,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -11917,6 +13551,8 @@ snapshots: transitivePeerDependencies: - supports-color + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -11929,8 +13565,16 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safe-regex2@5.0.0: dependencies: ret: 0.5.0 @@ -11966,8 +13610,16 @@ snapshots: secure-json-parse@4.1.0: {} + seek-bzip@1.0.6: + dependencies: + commander: 2.20.3 + semver@6.3.1: {} + semver@7.3.5: + dependencies: + lru-cache: 6.0.0 + semver@7.7.3: {} semver@7.7.4: {} @@ -11992,7 +13644,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -12153,6 +13805,16 @@ snapshots: std-env@3.10.0: {} + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + stream-to-it@1.0.1: + dependencies: + it-stream-types: 2.0.2 + streamsearch@1.1.0: {} strict-event-emitter@0.5.1: {} @@ -12169,10 +13831,18 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -12183,6 +13853,12 @@ snapshots: strip-bom@3.0.0: {} + strip-dirs@2.1.0: + dependencies: + is-natural-number: 4.0.1 + + strip-final-newline@2.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -12209,7 +13885,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.5 formidable: 3.5.4 @@ -12229,6 +13905,10 @@ snapshots: supports-color@10.2.2: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -12264,6 +13944,16 @@ snapshots: pump: 3.0.3 tar-stream: 2.2.0 + tar-stream@1.6.2: + dependencies: + bl: 1.2.3 + buffer-alloc: 1.2.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + readable-stream: 2.3.8 + to-buffer: 1.2.2 + xtend: 4.0.2 + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -12306,6 +13996,8 @@ snapshots: throat@5.0.0: {} + through@2.3.8: {} + thunky@1.1.0: {} time-span@5.1.0: @@ -12333,6 +14025,12 @@ snapshots: dependencies: tldts-core: 7.0.23 + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.5 + + tmp@0.2.5: {} + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -12426,6 +14124,8 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.21.3: {} + type-fest@0.7.1: {} type-fest@5.4.4: @@ -12458,7 +14158,7 @@ snapshots: app-root-path: 3.1.0 buffer: 6.0.3 dayjs: 1.11.19 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) dedent: 1.7.0 dotenv: 16.6.1 glob: 10.5.0 @@ -12496,8 +14196,15 @@ snapshots: dependencies: multiformats: 13.4.2 + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + undici-types@7.16.0: {} + undici@7.16.0: {} + undici@7.21.0: {} universalify@2.0.1: {} @@ -12534,6 +14241,8 @@ snapshots: dependencies: punycode: 2.3.1 + urlpattern-polyfill@10.1.0: {} + use-sync-external-store@1.6.0(react@19.2.4): dependencies: react: 19.2.4 @@ -12542,10 +14251,20 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + utils-merge@1.0.1: {} uuid@11.1.0: {} + uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} validator@13.15.23: {} @@ -12649,6 +14368,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + wabt@1.0.24: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -12667,6 +14388,39 @@ snapshots: ms: 3.0.0-canary.202508261828 supports-color: 10.2.2 + web3-errors@1.3.1: + dependencies: + web3-types: 1.10.0 + + web3-eth-abi@4.4.1(typescript@5.9.3)(zod@3.25.76): + dependencies: + abitype: 0.7.1(typescript@5.9.3)(zod@3.25.76) + web3-errors: 1.3.1 + web3-types: 1.10.0 + web3-utils: 4.3.3 + web3-validator: 2.0.6 + transitivePeerDependencies: + - typescript + - zod + + web3-types@1.10.0: {} + + web3-utils@4.3.3: + dependencies: + ethereum-cryptography: 2.2.1 + eventemitter3: 5.0.4 + web3-errors: 1.3.1 + web3-types: 1.10.0 + web3-validator: 2.0.6 + + web3-validator@2.0.6: + dependencies: + ethereum-cryptography: 2.2.1 + util: 0.12.5 + web3-errors: 1.3.1 + web3-types: 1.10.0 + zod: 3.25.76 + webcrypto-core@1.8.1: dependencies: '@peculiar/asn1-schema': 2.6.0 @@ -12759,6 +14513,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -12790,6 +14550,10 @@ snapshots: ws@8.19.0: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xml-name-validator@5.0.0: {} xml2js@0.6.2: @@ -12807,6 +14571,12 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + + yaml@1.10.3: {} + + yaml@2.8.1: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -12821,10 +14591,17 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yn@3.1.1: {} yocto-queue@1.2.2: {} yoctocolors-cjs@2.1.3: {} + zod@3.25.76: {} + zod@4.3.6: {} From a1c38dda5cad647a28298fee14359ae7ec3407dc Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 08:42:51 +0200 Subject: [PATCH 03/19] refactor(subgraph): trim schema to dealbot-queried fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete 12 entities that dealbot never queries: Service, ServiceProviderLink, EventLog, Transaction, FaultRecord, ProvingWindow, SumTreeCount, NetworkMetric, and the Weekly/MonthlyProviderActivity + Weekly/MonthlyProofSetActivity rollups. Trim Provider, DataSet, and Root to the fields backing the three backend queries in apps/backend/src/pdp-subgraph/queries.ts (GET_SUBGRAPH_META, GET_PROVIDERS_WITH_DATASETS, GET_FWSS_CANDIDATE_PIECES). graph codegen passes. graph build is intentionally broken by this commit — the next commit prunes handlers that reference deleted fields. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/subgraph/schema.graphql | 308 ++++------------------------------- 1 file changed, 33 insertions(+), 275 deletions(-) diff --git a/apps/subgraph/schema.graphql b/apps/subgraph/schema.graphql index d842b80e..3c95b855 100644 --- a/apps/subgraph/schema.graphql +++ b/apps/subgraph/schema.graphql @@ -5,301 +5,59 @@ enum DataSetStatus { DELETED # Dataset has been deleted on-chain } +type Provider @entity(immutable: false) { + id: Bytes! # address + address: Bytes! + # Proving-period tracking is strictly bound to the FWSS contract. + totalFaultedPeriods: BigInt! + totalProvingPeriods: BigInt! + # Derived relationship + proofSets: [DataSet!]! @derivedFrom(field: "owner") +} + type DataSet @entity(immutable: false) { id: Bytes! # setId - setId: BigInt! # uint256 - listener: Service! # address - owner: Provider! # address of the provider - leafCount: BigInt! # uint256 - challengeRange: BigInt! # uint256 + setId: BigInt! + owner: Provider! # True iff the dataset has not been deleted (PDPVerifier.DataSetDeleted) # and the FWSS service has not been terminated (FWSS.ServiceTerminated). - # Note: PDPPaymentTerminated does NOT affect this flag; use pdpPaymentEndEpoch. + # Note: PDPPaymentTerminated does NOT affect this flag; clients compare + # pdpPaymentEndEpoch to current epoch themselves. isActive: Boolean! status: DataSetStatus! - lastProvenEpoch: BigInt! # uint256 - nextChallengeEpoch: BigInt! # uint256 - # NOTE: Proving period tracking is strictly bound to FWSS contract - nextDeadline: BigInt! # Block number of next deadline (0 if not set) - firstDeadline: BigInt! # Block number of first NextProvingPeriod event (0 if not set) - maxProvingPeriod: BigInt! # Multiplier for proving period frequency (0 if not set) - challengeWindowSize: BigInt! # Size of challenge window before each deadline (0 if not set) - currentDeadlineCount: BigInt! # Current deadline count - provenThisPeriod: Boolean! - # Existing fields - totalRoots: BigInt! # uint256 - nextPieceId: BigInt! # uint256 - totalDataSize: BigInt! # uint256 - totalProofs: BigInt! # uint256 - totalProvedRoots: BigInt! # uint256 - totalFeePaid: BigInt! # uint256 - totalFaultedPeriods: BigInt! # uint256 - totalFaultedRoots: BigInt! # uint256 - totalTransactions: BigInt! # uint256 - totalEventLogs: BigInt! # uint256 - createdAt: BigInt! - updatedAt: BigInt! - blockNumber: BigInt! + # Block number of next deadline, 0 if not set. Dealbot filters on this + # to detect overdue proving periods. + nextDeadline: BigInt! + # Multiplier for proving period frequency, 0 if not set. + maxProvingPeriod: BigInt! - # ---- FWSS fields (null / empty for non-FWSS datasets) ---- + # ---- FWSS fields (null / false for non-FWSS datasets) ---- # Populated by FWSS.DataSetCreated handler. - fwssProviderId: BigInt # uint256 — FWSS numeric provider ID - fwssPayer: Bytes # address of the payer - fwssServiceProvider: Bytes # address — may diverge from owner after transfers - fwssPdpRailId: BigInt # uint256 — FWSS PDP rail ID - - # Raw metadata from DataSetCreated (kept for completeness; clients should - # prefer the derived booleans below for filter queries). - metadataKeys: [String!]! # empty array when not FWSS - metadataValues: [String!]! # empty array when not FWSS + fwssPayer: Bytes # address of the payer + fwssServiceProvider: Bytes # address — may diverge from owner after transfers - # Derived metadata flags (cheap to filter on). - withIPFSIndexing: Boolean! # true iff "withIPFSIndexing" in metadataKeys - withCDN: Boolean! # true iff "withCDN" in metadataKeys + # Derived from FWSS.DataSetCreated metadataKeys. + withIPFSIndexing: Boolean! # Populated by FWSS.PDPPaymentTerminated handler. # May be in the past (already terminated) or future (terminating). - # Does NOT flip isActive — clients that care must compare to current epoch. + # Does NOT flip isActive — clients compare to current epoch. pdpPaymentEndEpoch: BigInt - # Derived relationships - roots: [Root!]! @derivedFrom(field: "proofSet") - transactions: [Transaction!]! @derivedFrom(field: "proofSet") - eventLogs: [EventLog!]! @derivedFrom(field: "proofSet") - faultRecords: [FaultRecord!]! @derivedFrom(field: "proofSet") - provingWindows: [ProvingWindow!]! @derivedFrom(field: "proofSet") - weeklyActivities: [WeeklyProofSetActivity!]! @derivedFrom(field: "proofSet") - monthlyActivities: [MonthlyProofSetActivity!]! @derivedFrom(field: "proofSet") -} - -type Service @entity(immutable: false) { - id: Bytes! # address - address: Bytes! # Service Contract Address - totalProofSets: BigInt! # uint256 - totalProviders: BigInt! # uint256 - totalRoots: BigInt! # uint256 - totalDataSize: BigInt! # uint256 - totalFaultedRoots: BigInt! - totalFaultedPeriods: BigInt! - createdAt: BigInt! - updatedAt: BigInt! - # Relationships - proofSets: [DataSet!]! @derivedFrom(field: "listener") - providerLinks: [ServiceProviderLink!]! @derivedFrom(field: "service") -} - -type ServiceProviderLink @entity(immutable: false) { - id: Bytes! - totalProofSets: BigInt! - # Relationships - service: Service! - provider: Provider! -} - -type Provider @entity(immutable: false) { - id: Bytes! # address - address: Bytes! - # Calculation of Faulted and total proving periods is strictly bound to FWSS - totalFaultedPeriods: BigInt! # total faulted periods recorded out of totalProvingPeriods - totalProvingPeriods: BigInt! # total proving periods recorded - totalFaultedRoots: BigInt! - totalProofSets: BigInt! - totalRoots: BigInt! - totalDataSize: BigInt! - createdAt: BigInt! - updatedAt: BigInt! - blockNumber: BigInt! - # Derived relationship - proofSets: [DataSet!]! @derivedFrom(field: "owner") - serviceLinks: [ServiceProviderLink!]! @derivedFrom(field: "provider") - weeklyProviderActivities: [WeeklyProviderActivity!]! - @derivedFrom(field: "provider") - monthlyProviderActivities: [MonthlyProviderActivity!]! - @derivedFrom(field: "provider") + roots: [Root!]! @derivedFrom(field: "proofSet") } type Root @entity(immutable: false) { - id: Bytes! # Unique ID for Root (e.g., setId-rootId) - setId: BigInt! # uint256 (Keep for filtering/direct access) - rootId: BigInt! # uint256 - rawSize: BigInt! # uint256 - leafCount: BigInt! # uint256 + id: Bytes! # setId-rootId + setId: BigInt! + rootId: BigInt! + rawSize: BigInt! cid: Bytes! removed: Boolean! - totalProofsSubmitted: BigInt! # uint256 - totalPeriodsFaulted: BigInt! # uint256 - lastProvenEpoch: BigInt! # uint256 - lastProvenAt: BigInt! # uint256 - lastFaultedEpoch: BigInt! # uint256 - lastFaultedAt: BigInt! # uint256 - createdAt: BigInt! - updatedAt: BigInt! - blockNumber: BigInt! - - # ---- FWSS fields (null / empty for non-FWSS pieces) ---- - # Populated by FWSS.PieceAdded handler. - metadataKeys: [String!]! # empty array when not FWSS - metadataValues: [String!]! # empty array when not FWSS - ipfsRootCID: String # values[indexOf(keys, "ipfsRootCID")] or null - # Relationship - proofSet: DataSet! # Link to DataSet (stores DataSet ID) - # Derived relationships - faultRecords: [FaultRecord!]! @derivedFrom(field: "roots") # For many-to-many derived -} - -type SumTreeCount @entity(immutable: false) { - id: Bytes! # setId-rootId - setId: BigInt! # uint256 (Keep for filtering/direct access) - rootId: BigInt! # uint256 - count: BigInt! # uint256 - lastCount: BigInt! # uint256 - lastDecEpoch: BigInt! -} - -type EventLog @entity(immutable: true) { - id: Bytes! # transactionHash-logIndex - setId: BigInt! # uint256 (Keep for filtering/direct access) - address: Bytes! - name: String! - data: String! - logIndex: BigInt! - transactionHash: Bytes! # Keep for linking - createdAt: BigInt! - blockNumber: BigInt! - - # Relationships - proofSet: DataSet! # Link to DataSet (stores DataSet ID) - transaction: Transaction! # Link to Transaction (stores Transaction hash) -} - -type Transaction @entity(immutable: true) { - id: Bytes! # hash - hash: Bytes! - dataSetId: BigInt! # uint256 (Keep for filtering/direct access) - height: BigInt! # uint256 - fromAddress: Bytes! # address - toAddress: Bytes # address - value: BigInt! # uint256 - method: String! - status: Boolean! - createdAt: BigInt! - - # Relationship - proofSet: DataSet! # Link to DataSet (stores DataSet ID) - # Derived relationship - eventLogs: [EventLog!]! @derivedFrom(field: "transaction") -} - -type FaultRecord @entity(immutable: true) { - id: Bytes! # Unique ID (e.g., txHash-logIndex) - dataSetId: BigInt! # uint256 (Keep for filtering) - pieceIds: [BigInt!]! # uint256[] (Keep for direct access) - currentChallengeEpoch: BigInt! # uint256 - nextChallengeEpoch: BigInt! # uint256 - periodsFaulted: BigInt! # uint256 - deadline: BigInt! # uint256 - createdAt: BigInt! - blockNumber: BigInt! - - # Relationships - proofSet: DataSet! # Link to DataSet (stores DataSet ID) - roots: [Root!]! # Link to Pieces (stores array of Root IDs) -} - -type ProvingWindow @entity(immutable: false) { - id: Bytes! # setId-deadlineCount - setId: BigInt! # uint256 - deadlineCount: BigInt! # Which deadline this represents - deadline: BigInt! # Block number of the deadline - windowStart: BigInt! # Block number when challenge window starts - windowEnd: BigInt! # Block number when challenge window ends (same as deadline) - proofSubmitted: Boolean! # Whether a valid proof was submitted in this window - proofBlockNumber: BigInt! # Block number when proof was submitted (0 if no proof) - isValid: Boolean! # Whether proof was submitted within the valid window - createdAt: BigInt! - - # Relationship - proofSet: DataSet! # Link to DataSet -} - -# Metrices - -type NetworkMetric @entity(immutable: false) { - id: Bytes! # Unique ID (e.g., txHash-logIndex) - totalProofSets: BigInt # uint256 - totalActiveProofSets: BigInt # uint256 - totalProviders: BigInt # uint256 - totalRoots: BigInt # uint256 - totalActiveRoots: BigInt # uint256 - totalDataSize: BigInt # uint256 - totalProofFeePaidInFil: BigInt # uint256 - totalProofs: BigInt # uint256 - totalProvedRoots: BigInt # uint256 - totalFaultedPeriods: BigInt # uint256 - totalFaultedRoots: BigInt # uint256 - totalServices: BigInt # uint256 -} - -type WeeklyProviderActivity @entity(immutable: false) { - id: Bytes! # Unique ID (e.g., time derived) - providerId: Bytes! # address (Keep for filtering) - totalRootsAdded: BigInt! # uint256 - totalDataSizeAdded: BigInt! # uint256 - totalRootsRemoved: BigInt! # uint256 - totalDataSizeRemoved: BigInt! # uint256 - totalProofSetsCreated: BigInt! # uint256 - totalProofs: BigInt! # uint256 - totalRootsProved: BigInt! # uint256 - totalFaultedRoots: BigInt! # uint256 - totalFaultedPeriods: BigInt! # uint256 - # Relationships - provider: Provider! # Link to Provider (stores Provider ID) -} - -type MonthlyProviderActivity @entity(immutable: false) { - id: Bytes! # Unique ID (e.g., time derived) - providerId: Bytes! # address (Keep for filtering) - totalProofSetsCreated: BigInt! # uint256 - totalRootsAdded: BigInt! # uint256 - totalDataSizeAdded: BigInt! # uint256 - totalRootsRemoved: BigInt! # uint256 - totalDataSizeRemoved: BigInt! # uint256 - totalProofs: BigInt! # uint256 - totalRootsProved: BigInt! # uint256 - totalFaultedRoots: BigInt! # uint256 - totalFaultedPeriods: BigInt! # uint256 - # Relationships - provider: Provider! # Link to Provider (stores Provider ID) -} - -type WeeklyProofSetActivity @entity(immutable: false) { - id: Bytes! # Unique ID (e.g., time derived) - dataSetId: BigInt! # uint256 (Keep for filtering) - totalRootsAdded: BigInt! # uint256 - totalDataSizeAdded: BigInt! # uint256 - totalRootsRemoved: BigInt! # uint256 - totalDataSizeRemoved: BigInt! # uint256 - totalProofs: BigInt! # uint256 - totalRootsProved: BigInt! # uint256 - totalFaultedRoots: BigInt! # uint256 - totalFaultedPeriods: BigInt! # uint256 - # Relationships - proofSet: DataSet! # Link to DataSet (stores DataSet ID) -} + # Populated by FWSS.PieceAdded handler (null for non-FWSS pieces). + ipfsRootCID: String -type MonthlyProofSetActivity @entity(immutable: false) { - id: Bytes! # Unique ID (e.g., time derived) - dataSetId: BigInt! # uint256 (Keep for filtering) - totalRootsAdded: BigInt! # uint256 - totalDataSizeAdded: BigInt! # uint256 - totalRootsRemoved: BigInt! # uint256 - totalDataSizeRemoved: BigInt! # uint256 - totalProofs: BigInt! # uint256 - totalRootsProved: BigInt! # uint256 - totalFaultedRoots: BigInt! # uint256 - totalFaultedPeriods: BigInt! # uint256 - # Relationships - proofSet: DataSet! # Link to DataSet (stores DataSet ID) + proofSet: DataSet! } From 09de7a819c41d4d3a69cb78ccd44c821cab52090 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 08:52:35 +0200 Subject: [PATCH 04/19] refactor(subgraph): prune handlers to surviving schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut ~3000 LOC of handler code that wrote to deleted entities: - Delete helper.ts, sumTree.ts, pdp-service.ts (latter was never wired in subgraph.yaml in the first place). - Delete abis/PDPService.json (unreferenced). - Rewrite pdp-verifier.ts from 1603 → 233 LOC. Drop EventLog, Transaction, Service, ServiceProviderLink bookkeeping and the weekly/monthly metrics rollups. Delete handleProofFeePaid entirely (fee not queried). Keep handlePossessionProven as a one-line flag flipper — needed because handleNextProvingPeriod uses provenThisPeriod to classify skipped periods as faults. - Trim fwss.ts to populate only the six FWSS fields still on the schema (fwssPayer, fwssServiceProvider, withIPFSIndexing, pdpPaymentEndEpoch, plus ipfsRootCID on Root). - Add provenThisPeriod back to DataSet schema — internal signal for the faultedPeriods computation; not directly queried. - Drop ProofFeePaid binding and unused entity names from manifest. - Shrink utils/index.ts to just MaxProvingPeriod. graph codegen and graph build pass. graph test fails on cases that still reference deleted fields — task-04 rewrites tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/subgraph/abis/PDPService.json | 649 ----------- apps/subgraph/schema.graphql | 4 + apps/subgraph/src/fwss.ts | 108 +- apps/subgraph/src/helper.ts | 238 ---- apps/subgraph/src/pdp-service.ts | 371 ------- apps/subgraph/src/pdp-verifier.ts | 1610 +++------------------------- apps/subgraph/src/sumTree.ts | 200 ---- apps/subgraph/subgraph.yaml | 10 - apps/subgraph/utils/index.ts | 19 +- 9 files changed, 148 insertions(+), 3061 deletions(-) delete mode 100644 apps/subgraph/abis/PDPService.json delete mode 100644 apps/subgraph/src/helper.ts delete mode 100644 apps/subgraph/src/pdp-service.ts delete mode 100644 apps/subgraph/src/sumTree.ts diff --git a/apps/subgraph/abis/PDPService.json b/apps/subgraph/abis/PDPService.json deleted file mode 100644 index b2ab5d30..00000000 --- a/apps/subgraph/abis/PDPService.json +++ /dev/null @@ -1,649 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "NO_CHALLENGE_SCHEDULED", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "NO_PROVING_DEADLINE", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "UPGRADE_INTERFACE_VERSION", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "challengeWindow", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "dataSetCreated", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "creator", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "dataSetDeleted", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "deletedLeafCount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getChallengesPerProof", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint64", - "internalType": "uint64" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "getMaxProvingPeriod", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint64", - "internalType": "uint64" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "getPDPConfig", - "inputs": [], - "outputs": [ - { - "name": "maxProvingPeriod", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "challengeWindow_", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "challengesPerProof", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "initChallengeWindowStart_", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "initChallengeWindowStart", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "initialize", - "inputs": [ - { - "name": "_pdpVerifierAddress", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "nextChallengeWindowStart", - "inputs": [ - { - "name": "setId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "nextPDPChallengeWindowStart", - "inputs": [ - { - "name": "setId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "nextProvingPeriod", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "challengeEpoch", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "owner", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "pdpVerifierAddress", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "piecesAdded", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "firstAdded", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "pieceData", - "type": "tuple[]", - "internalType": "struct Cids.Cid[]", - "components": [ - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "piecesScheduledRemove", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "pieceIds", - "type": "uint256[]", - "internalType": "uint256[]" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "possessionProven", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "challengeCount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "provenThisPeriod", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "provingDeadlines", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "proxiableUUID", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "renounceOwnership", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "storageProviderChanged", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "thisChallengeWindowStart", - "inputs": [ - { - "name": "setId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "transferOwnership", - "inputs": [ - { - "name": "newOwner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "upgradeToAndCall", - "inputs": [ - { - "name": "newImplementation", - "type": "address", - "internalType": "address" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "event", - "name": "FaultRecord", - "inputs": [ - { - "name": "dataSetId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "periodsFaulted", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "deadline", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Initialized", - "inputs": [ - { - "name": "version", - "type": "uint64", - "indexed": false, - "internalType": "uint64" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "OwnershipTransferred", - "inputs": [ - { - "name": "previousOwner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "newOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Upgraded", - "inputs": [ - { - "name": "implementation", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "AddressEmptyCode", - "inputs": [ - { - "name": "target", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC1967InvalidImplementation", - "inputs": [ - { - "name": "implementation", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC1967NonPayable", - "inputs": [] - }, - { - "type": "error", - "name": "FailedCall", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidInitialization", - "inputs": [] - }, - { - "type": "error", - "name": "NotInitializing", - "inputs": [] - }, - { - "type": "error", - "name": "OwnableInvalidOwner", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "OwnableUnauthorizedAccount", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "UUPSUnauthorizedCallContext", - "inputs": [] - }, - { - "type": "error", - "name": "UUPSUnsupportedProxiableUUID", - "inputs": [ - { - "name": "slot", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } -] diff --git a/apps/subgraph/schema.graphql b/apps/subgraph/schema.graphql index 3c95b855..1dadcf57 100644 --- a/apps/subgraph/schema.graphql +++ b/apps/subgraph/schema.graphql @@ -30,6 +30,10 @@ type DataSet @entity(immutable: false) { nextDeadline: BigInt! # Multiplier for proving period frequency, 0 if not set. maxProvingPeriod: BigInt! + # Internal: flipped true by PossessionProven, reset false on each + # NextProvingPeriod. Drives the faultedPeriods counter on Provider; + # not directly queried by dealbot. + provenThisPeriod: Boolean! # ---- FWSS fields (null / false for non-FWSS datasets) ---- # Populated by FWSS.DataSetCreated handler. diff --git a/apps/subgraph/src/fwss.ts b/apps/subgraph/src/fwss.ts index 983a151e..1012a0e1 100644 --- a/apps/subgraph/src/fwss.ts +++ b/apps/subgraph/src/fwss.ts @@ -1,14 +1,14 @@ import { BigInt, Bytes, log } from "@graphprotocol/graph-ts"; import { DataSetCreated as DataSetCreatedEvent, + DataSetServiceProviderChanged as DataSetServiceProviderChangedEvent, + PDPPaymentTerminated as PDPPaymentTerminatedEvent, PieceAdded as PieceAddedEvent, ServiceTerminated as ServiceTerminatedEvent, - PDPPaymentTerminated as PDPPaymentTerminatedEvent, - DataSetServiceProviderChanged as DataSetServiceProviderChangedEvent, } from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService"; import { DataSet, Root } from "../generated/schema"; import { getRootEntityId } from "./pdp-verifier"; -import { saveNetworkMetrics } from "./helper"; +import { DataSetStatus } from "./types"; // ---- Helpers -------------------------------------------------------------- @@ -23,11 +23,7 @@ function arrayContains(arr: string[], needle: string): boolean { return false; } -function extractMetadataValue( - keys: string[], - values: string[], - needle: string -): string | null { +function extractMetadataValue(keys: string[], values: string[], needle: string): string | null { for (let i = 0; i < keys.length; i++) { if (keys[i] == needle) { return i < values.length ? values[i] : null; @@ -51,56 +47,23 @@ export function handleFwssDataSetCreated(event: DataSetCreatedEvent): void { if (ds == null) { ds = new DataSet(id); ds.setId = event.params.dataSetId; - // PDPVerifier-level non-null fields — safe defaults; handleDataSetCreated - // will overwrite shortly after in this same block. + // PDPVerifier-level non-null defaults; handleDataSetCreated will overwrite. ds.owner = event.params.serviceProvider; - ds.listener = event.address; ds.isActive = true; - ds.leafCount = BigInt.fromI32(0); - ds.challengeRange = BigInt.fromI32(0); - ds.lastProvenEpoch = BigInt.fromI32(0); - ds.nextChallengeEpoch = BigInt.fromI32(0); - ds.firstDeadline = BigInt.fromI32(0); - ds.maxProvingPeriod = BigInt.fromI32(0); - ds.challengeWindowSize = BigInt.fromI32(0); - ds.currentDeadlineCount = BigInt.fromI32(0); - ds.nextDeadline = BigInt.fromI32(0); + ds.status = DataSetStatus.EMPTY; + ds.nextDeadline = BigInt.zero(); + ds.maxProvingPeriod = BigInt.zero(); ds.provenThisPeriod = false; - ds.totalRoots = BigInt.fromI32(0); - ds.nextPieceId = BigInt.fromI32(0); - ds.totalDataSize = BigInt.fromI32(0); - ds.totalFeePaid = BigInt.fromI32(0); - ds.totalFaultedPeriods = BigInt.fromI32(0); - ds.totalFaultedRoots = BigInt.fromI32(0); - ds.totalProofs = BigInt.fromI32(0); - ds.totalProvedRoots = BigInt.fromI32(0); - ds.totalTransactions = BigInt.fromI32(0); - ds.totalEventLogs = BigInt.fromI32(0); - ds.createdAt = event.block.timestamp; - ds.blockNumber = event.block.number; - // status: EMPTY. Imported enum value would be cleaner, but schema.graphql - // defines the enum; matching literal is what the generated code stores. - ds.status = "EMPTY"; } - ds.fwssProviderId = event.params.providerId; ds.fwssPayer = event.params.payer; ds.fwssServiceProvider = event.params.serviceProvider; - ds.fwssPdpRailId = event.params.pdpRailId; - ds.metadataKeys = event.params.metadataKeys; - ds.metadataValues = event.params.metadataValues; - ds.withIPFSIndexing = arrayContains( - event.params.metadataKeys, - "withIPFSIndexing" - ); - ds.withCDN = arrayContains(event.params.metadataKeys, "withCDN"); - ds.updatedAt = event.block.timestamp; + ds.withIPFSIndexing = arrayContains(event.params.metadataKeys, "withIPFSIndexing"); ds.save(); } export function handleFwssPieceAdded(event: PieceAddedEvent): void { - const id = getRootEntityId(event.params.dataSetId, event.params.pieceId); - const root = Root.load(id); + const root = Root.load(getRootEntityId(event.params.dataSetId, event.params.pieceId)); if (root == null) { log.warning("FWSS PieceAdded for unknown root {}-{}", [ event.params.dataSetId.toString(), @@ -109,66 +72,34 @@ export function handleFwssPieceAdded(event: PieceAddedEvent): void { return; } - root.metadataKeys = event.params.keys; - root.metadataValues = event.params.values; - root.ipfsRootCID = extractMetadataValue( - event.params.keys, - event.params.values, - "ipfsRootCID" - ); - root.updatedAt = event.block.timestamp; + root.ipfsRootCID = extractMetadataValue(event.params.keys, event.params.values, "ipfsRootCID"); root.save(); } -export function handleFwssServiceTerminated( - event: ServiceTerminatedEvent -): void { - const id = getProofSetEntityId(event.params.dataSetId); - const ds = DataSet.load(id); +export function handleFwssServiceTerminated(event: ServiceTerminatedEvent): void { + const ds = DataSet.load(getProofSetEntityId(event.params.dataSetId)); if (ds == null) { - log.warning("FWSS ServiceTerminated for unknown dataSet {}", [ - event.params.dataSetId.toString(), - ]); + log.warning("FWSS ServiceTerminated for unknown dataSet {}", [event.params.dataSetId.toString()]); return; } - // Guard against double-decrement of totalActiveProofSets in case both - // DataSetDeleted (PDPVerifier) and ServiceTerminated (FWSS) fire for the - // same dataset. Only decrement if this event is the one flipping isActive. - if (ds.isActive) { - saveNetworkMetrics( - ["totalActiveProofSets"], - [BigInt.fromI32(1)], - ["subtract"] - ); - } ds.isActive = false; - ds.updatedAt = event.block.timestamp; ds.save(); } -export function handleFwssPdpPaymentTerminated( - event: PDPPaymentTerminatedEvent -): void { - const id = getProofSetEntityId(event.params.dataSetId); - const ds = DataSet.load(id); +export function handleFwssPdpPaymentTerminated(event: PDPPaymentTerminatedEvent): void { + const ds = DataSet.load(getProofSetEntityId(event.params.dataSetId)); if (ds == null) { - log.warning("FWSS PDPPaymentTerminated for unknown dataSet {}", [ - event.params.dataSetId.toString(), - ]); + log.warning("FWSS PDPPaymentTerminated for unknown dataSet {}", [event.params.dataSetId.toString()]); return; } ds.pdpPaymentEndEpoch = event.params.endEpoch; - ds.updatedAt = event.block.timestamp; ds.save(); } -export function handleFwssDataSetServiceProviderChanged( - event: DataSetServiceProviderChangedEvent -): void { - const id = getProofSetEntityId(event.params.dataSetId); - const ds = DataSet.load(id); +export function handleFwssDataSetServiceProviderChanged(event: DataSetServiceProviderChangedEvent): void { + const ds = DataSet.load(getProofSetEntityId(event.params.dataSetId)); if (ds == null) { log.warning("FWSS DataSetServiceProviderChanged for unknown dataSet {}", [ event.params.dataSetId.toString(), @@ -177,6 +108,5 @@ export function handleFwssDataSetServiceProviderChanged( } ds.fwssServiceProvider = event.params.newServiceProvider; - ds.updatedAt = event.block.timestamp; ds.save(); } diff --git a/apps/subgraph/src/helper.ts b/apps/subgraph/src/helper.ts deleted file mode 100644 index ed8df5ef..00000000 --- a/apps/subgraph/src/helper.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { - BigInt, - Bytes, - Value, - log, - store, - Entity, -} from "@graphprotocol/graph-ts"; -import { NetworkMetric } from "../generated/schema"; - -export function saveNetworkMetrics( - keys: string[], - values: BigInt[], - methods?: string[] -): void { - const networkMetric = NetworkMetric.load(Bytes.fromUTF8("pdp_network_stats")); - - if (networkMetric) { - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const method = methods ? methods[i] : "add"; - - const valueToChangeValue = networkMetric.get(key); - let valueToChange = BigInt.fromI32(0); - if (valueToChangeValue) { - valueToChange = valueToChangeValue.toBigInt(); - } - if (method == "add") { - networkMetric.set(key, Value.fromBigInt(valueToChange.plus(value))); - } else if (method == "subtract") { - networkMetric.set(key, Value.fromBigInt(valueToChange.minus(value))); - } else { - networkMetric.set(key, Value.fromBigInt(valueToChange.plus(value))); - } - } - networkMetric.save(); - } else { - const networkMetric = new NetworkMetric( - Bytes.fromUTF8("pdp_network_stats") - ); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const method = methods ? methods[i] : "add"; - - let valueToAdd = BigInt.fromI32(0); - if (method == "add") { - valueToAdd = value; - } else if (method == "subtract") { - valueToAdd = BigInt.fromI32(0); - } else { - valueToAdd = value; - } - - networkMetric.set(key, Value.fromBigInt(valueToAdd)); - } - networkMetric.save(); - } -} - -export function saveProviderMetrics( - entity: string, - id: Bytes, - providerId: Bytes, - keys: string[], - values: BigInt[], - methods?: string[] -): void { - const availableEntities = [ - "WeeklyProviderActivity", - "MonthlyProviderActivity", - ]; - if (!availableEntities.includes(entity)) { - log.error("Invalid entity: {}", [entity]); - return; - } - - const entityInstance = store.get(entity, id.toHexString()); - if (entityInstance) { - entityInstance.set("providerId", Value.fromBytes(providerId)); - entityInstance.set("provider", Value.fromBytes(providerId)); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const method = methods ? methods[i] : "add"; - - const valueToChangeValue = entityInstance.get(key); - let valueToChange = BigInt.fromI32(0); - if (valueToChangeValue) { - valueToChange = valueToChangeValue.toBigInt(); - } - if (method == "replace") { - entityInstance.set(key, Value.fromBigInt(value)); - } else if (method == "add") { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } else if (method == "subtract") { - entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); - } else { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } - } - store.set(entity, id.toHexString(), entityInstance); - } else { - let requiredKeys = [ - "totalRootsAdded", - "totalDataSizeAdded", - "totalProofSetsCreated", - "totalProofs", - "totalRootsProved", - "totalFaultedRoots", - "totalFaultedPeriods", - "totalRootsRemoved", - "totalDataSizeRemoved", - ]; - const entityInstance = new Entity(); - entityInstance.set("id", Value.fromBytes(id)); - entityInstance.set("providerId", Value.fromBytes(providerId)); - entityInstance.set("provider", Value.fromBytes(providerId)); - for (let i = 0; i < requiredKeys.length; i++) { - entityInstance.set(requiredKeys[i], Value.fromBigInt(BigInt.fromI32(0))); - } - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const method = methods ? methods[i] : "add"; - - const valueToChangeValue = entityInstance.get(key); - let valueToChange = BigInt.fromI32(0); - if (valueToChangeValue) { - valueToChange = valueToChangeValue.toBigInt(); - } - if (method == "replace") { - entityInstance.set(key, Value.fromBigInt(value)); - } else if (method == "add") { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } else if (method == "subtract") { - entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); - } else { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } - } - store.set(entity, id.toHexString(), entityInstance); - } -} - -export function saveProofSetMetrics( - entity: string, - id: Bytes, - dataSetId: BigInt, - keys: string[], - values: BigInt[], - methods?: string[] -): void { - const availableEntities = [ - "WeeklyProofSetActivity", - "MonthlyProofSetActivity", - ]; - if (!availableEntities.includes(entity)) { - log.error("Invalid entity: {}", [entity]); - return; - } - - const entityInstance = store.get(entity, id.toHexString()); - - if (entityInstance) { - entityInstance.set("dataSetId", Value.fromBigInt(dataSetId)); - entityInstance.set( - "proofSet", - Value.fromBytes(Bytes.fromByteArray(Bytes.fromBigInt(dataSetId))) - ); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const method = methods ? methods[i] : "add"; - - const valueToChangeValue = entityInstance.get(key); - let valueToChange = BigInt.fromI32(0); - if (valueToChangeValue) { - valueToChange = valueToChangeValue.toBigInt(); - } - if (method == "replace") { - entityInstance.set(key, Value.fromBigInt(value)); - } else if (method == "add") { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } else if (method == "subtract") { - entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); - } else { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } - } - store.set(entity, id.toHexString(), entityInstance); - } else { - let requiredKeys = [ - "totalRootsAdded", - "totalDataSizeAdded", - "totalProofs", - "totalRootsProved", - "totalFaultedRoots", - "totalFaultedPeriods", - "totalRootsRemoved", - "totalDataSizeRemoved", - ]; - const entityInstance = new Entity(); - entityInstance.set("id", Value.fromBytes(id)); - entityInstance.set("dataSetId", Value.fromBigInt(dataSetId)); - entityInstance.set( - "proofSet", - Value.fromBytes(Bytes.fromByteArray(Bytes.fromBigInt(dataSetId))) - ); - for (let i = 0; i < requiredKeys.length; i++) { - entityInstance.set(requiredKeys[i], Value.fromBigInt(BigInt.fromI32(0))); - } - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const method = methods ? methods[i] : "add"; - - const valueToChangeValue = entityInstance.get(key); - let valueToChange = BigInt.fromI32(0); - if (valueToChangeValue) { - valueToChange = valueToChangeValue.toBigInt(); - } - if (method == "replace") { - entityInstance.set(key, Value.fromBigInt(value)); - } else if (method == "add") { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } else if (method == "subtract") { - entityInstance.set(key, Value.fromBigInt(valueToChange.minus(value))); - } else { - entityInstance.set(key, Value.fromBigInt(valueToChange.plus(value))); - } - } - store.set(entity, id.toHexString(), entityInstance); - } -} diff --git a/apps/subgraph/src/pdp-service.ts b/apps/subgraph/src/pdp-service.ts deleted file mode 100644 index bde3508b..00000000 --- a/apps/subgraph/src/pdp-service.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { BigInt, Bytes, crypto, Address, log } from "@graphprotocol/graph-ts"; -import { FaultRecord as FaultRecordEvent } from "../generated/PDPService/PDPService"; -import { PDPVerifier } from "../generated/PDPVerifier/PDPVerifier"; -import { PDPVerifierAddress, NumChallenges } from "../utils"; -import { - EventLog, - DataSet, - Provider, - FaultRecord, - Root, - Service, -} from "../generated/schema"; -import { - saveNetworkMetrics, - saveProviderMetrics, - saveProofSetMetrics, -} from "./helper"; -import { SumTree } from "./sumTree"; - -// --- Helper Functions -function getProofSetEntityId(setId: BigInt): Bytes { - return Bytes.fromByteArray(Bytes.fromBigInt(setId)); -} - -function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { - return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); -} - -function getTransactionEntityId(txHash: Bytes): Bytes { - return txHash; -} - -function getEventLogEntityId(txHash: Bytes, logIndex: BigInt): Bytes { - return txHash.concatI32(logIndex.toI32()); -} -// --- End Helper Functions - -/** - * Pads a Buffer or Uint8Array to 32 bytes with leading zeros. - */ -function padTo32Bytes(input: Uint8Array): Uint8Array { - if (input.length >= 32) return input; - const out = new Uint8Array(32); - out.set(input, 32 - input.length); - return out; -} - -/** - * Generates a deterministic challenge index using seed, proofSetID, proofIndex, and totalLeaves. - * Mirrors the logic from Go's generateChallengeIndex. - */ -export function generateChallengeIndex( - seed: Uint8Array, - proofSetID: BigInt, - proofIndex: i32, - totalLeaves: BigInt -): BigInt { - const data = new Uint8Array(32 + 32 + 8); - - const paddedSeed = padTo32Bytes(seed); - data.set(paddedSeed, 0); - - // Convert proofSetID to Bytes and pad to 32 bytes (Big-Endian padding implied by padTo32Bytes) - const psIDBytes = Bytes.fromBigInt(proofSetID); - const psIDPadded = padTo32Bytes(psIDBytes); - data.set(psIDPadded, 32); // Write 32 bytes at offset 32 - - // Convert proofIndex (i32) to an 8-byte Uint8Array (uint64 Big-Endian) - const idxBuf = new Uint8Array(8); // Create 8-byte buffer, initialized to zeros - idxBuf[7] = u8(proofIndex & 0xff); // Least significant byte - idxBuf[6] = u8((proofIndex >> 8) & 0xff); - idxBuf[5] = u8((proofIndex >> 16) & 0xff); - idxBuf[4] = u8((proofIndex >> 24) & 0xff); // Most significant byte of the i32 - - data.set(idxBuf, 64); // Write the 8 bytes at offset 64 - - const hashBytes = crypto.keccak256(Bytes.fromUint8Array(data)); - // hashBytes is big-endian, so expected to be reversed - const hashIntUnsignedR = BigInt.fromUnsignedBytes( - Bytes.fromUint8Array(Bytes.fromHexString(hashBytes.toHexString()).reverse()) - ); - - if (totalLeaves.isZero()) { - log.error( - "generateChallengeIndex: totalLeaves is zero, cannot calculate modulus. ProofSetID: {}. Seed: {}", - [proofSetID.toString(), Bytes.fromUint8Array(seed).toHex()] - ); - return BigInt.fromI32(0); - } - - const challengeIndex = hashIntUnsignedR.mod(totalLeaves); - return challengeIndex; -} - -export function ensureEvenHex(value: BigInt): string { - const hexRaw = value.toHex().slice(2); - let paddedHex = hexRaw; - if (hexRaw.length % 2 === 1) { - paddedHex = "0" + hexRaw; - } - return "0x" + paddedHex; -} - -export function findChallengedRoots( - dataSetId: BigInt, - nextPieceId: BigInt, - challengeEpoch: BigInt, - totalLeaves: BigInt, - blockNumber: BigInt -): BigInt[] { - const instance = PDPVerifier.bind( - Address.fromBytes(Bytes.fromHexString(PDPVerifierAddress)) - ); - - const seedIntResult = instance.try_getRandomness(challengeEpoch); - if (seedIntResult.reverted) { - log.warning("findChallengedRoots: Failed to get randomness for epoch {}", [ - challengeEpoch.toString(), - ]); - return []; - } - - const seedInt = seedIntResult.value; - const seedHex = ensureEvenHex(seedInt); - - const challenges: BigInt[] = []; - if (totalLeaves.isZero()) { - log.warning( - "findChallengedRoots: totalLeaves is zero for DataSet {}. Cannot generate challenges.", - [dataSetId.toString()] - ); - return []; - } - for (let i = 0; i < NumChallenges; i++) { - const leafIdx = generateChallengeIndex( - Bytes.fromHexString(seedHex), - dataSetId, - i32(i), - totalLeaves - ); - challenges.push(leafIdx); - } - - const sumTreeInstance = new SumTree(); - const pieceIds = sumTreeInstance.findPieceIds( - dataSetId.toI32(), - nextPieceId.toI32(), - challenges, - blockNumber - ); - if (!pieceIds) { - log.warning("findChallengedRoots: findPieceIds reverted for dataSetId {}", [ - dataSetId.toString(), - ]); - return []; - } - - const rootIdsArray: BigInt[] = []; - for (let i = 0; i < pieceIds.length; i++) { - rootIdsArray.push(pieceIds[i].rootId); - } - return rootIdsArray; -} - -// Updated Handler -export function handleFaultRecord(event: FaultRecordEvent): void { - const setId = event.params.dataSetId; - const periodsFaultedParam = event.params.periodsFaulted; - const proofSetEntityId = getProofSetEntityId(setId); - const entityId = getEventLogEntityId(event.transaction.hash, event.logIndex); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - const proofSet = DataSet.load(proofSetEntityId); - if (!proofSet) { - log.warning("handleFaultRecord: DataSet {} not found for event tx {}", [ - setId.toString(), - event.transaction.hash.toHex(), - ]); - return; - } - const challengeEpoch = proofSet.nextChallengeEpoch; - const challengeRange = proofSet.challengeRange; - const proofSetOwner = proofSet.owner; - const nextPieceId = proofSet.totalRoots; - - const eventLog = new EventLog(entityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "FaultRecord"; - eventLog.data = `{"dataSetId":"${setId.toString()}","periodsFaulted":"${periodsFaultedParam.toString()}","deadline":"${event.params.deadline.toString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - - let nextChallengeEpoch = BigInt.fromI32(0); - const inputData = event.transaction.input; - if (inputData.length >= 4 + 32) { - const potentialNextEpochBytes = inputData.slice(4 + 32, 4 + 32 + 32); - if (potentialNextEpochBytes.length == 32) { - // Convert reversed Uint8Array to Bytes before converting to BigInt - nextChallengeEpoch = BigInt.fromUnsignedBytes( - Bytes.fromUint8Array(potentialNextEpochBytes.reverse()) - ); - } - } else { - log.warning( - "handleFaultRecord: Transaction input data too short to parse potential nextChallengeEpoch.", - [] - ); - } - - const pieceIds = findChallengedRoots( - setId, - nextPieceId, - challengeEpoch, - challengeRange, - event.block.number - ); - - if (pieceIds.length === 0) { - log.info( - "handleFaultRecord: No roots found for challenge epoch {} in DataSet {}", - [challengeEpoch.toString(), setId.toString()] - ); - } - - let uniqueRootIds: BigInt[] = []; - let rootIdMap = new Map(); - for (let i = 0; i < pieceIds.length; i++) { - const rootIdStr = pieceIds[i].toString(); - if (!rootIdMap.has(rootIdStr)) { - uniqueRootIds.push(pieceIds[i]); - rootIdMap.set(rootIdStr, true); - } - } - - let rootEntityIds: Bytes[] = []; - for (let i = 0; i < uniqueRootIds.length; i++) { - const rootId = uniqueRootIds[i]; - const rootEntityId = getRootEntityId(setId, rootId); - - const root = Root.load(rootEntityId); - if (root) { - if (!root.lastFaultedEpoch.equals(challengeEpoch)) { - root.totalPeriodsFaulted = - root.totalPeriodsFaulted.plus(periodsFaultedParam); - } else { - log.info( - "handleFaultRecord: Root {} in Set {} already marked faulted for epoch {}", - [rootId.toString(), setId.toString(), challengeEpoch.toString()] - ); - } - root.lastFaultedEpoch = challengeEpoch; - root.lastFaultedAt = event.block.timestamp; - root.updatedAt = event.block.timestamp; - root.blockNumber = event.block.number; - root.save(); - } else { - log.warning( - "handleFaultRecord: Root {} for Set {} not found while recording fault", - [rootId.toString(), setId.toString()] - ); - } - rootEntityIds.push(rootEntityId); - } - - const faultRecord = new FaultRecord(entityId); - faultRecord.dataSetId = setId; - faultRecord.pieceIds = uniqueRootIds; - faultRecord.currentChallengeEpoch = challengeEpoch; - faultRecord.nextChallengeEpoch = nextChallengeEpoch; - faultRecord.periodsFaulted = periodsFaultedParam; - faultRecord.deadline = event.params.deadline; - faultRecord.createdAt = event.block.timestamp; - faultRecord.blockNumber = event.block.number; - - faultRecord.proofSet = proofSetEntityId; - faultRecord.roots = rootEntityIds; - - faultRecord.save(); - eventLog.save(); - - proofSet.totalFaultedPeriods = - proofSet.totalFaultedPeriods.plus(periodsFaultedParam); - proofSet.totalFaultedRoots = proofSet.totalFaultedRoots.plus( - BigInt.fromI32(uniqueRootIds.length) - ); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; - proofSet.save(); - - const provider = Provider.load(proofSetOwner); - if (provider) { - provider.totalFaultedPeriods = - provider.totalFaultedPeriods.plus(periodsFaultedParam); - provider.totalFaultedRoots = provider.totalFaultedRoots.plus( - BigInt.fromI32(uniqueRootIds.length) - ); - provider.updatedAt = event.block.timestamp; - provider.blockNumber = event.block.number; - provider.save(); - } else { - log.warning("handleFaultRecord: Provider {} not found for DataSet {}", [ - proofSetOwner.toHex(), - setId.toString(), - ]); - } - - // update Service stats - const service = Service.load(proofSet.listener); - if (service) { - service.totalFaultedPeriods = - service.totalFaultedPeriods.plus(periodsFaultedParam); - service.totalFaultedRoots = service.totalFaultedRoots.plus( - BigInt.fromI32(uniqueRootIds.length) - ); - service.updatedAt = event.block.number; - service.save(); - } - - // Update network metrics - const keys = ["totalFaultedPeriods", "totalFaultedRoots"]; - const values = [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)]; - const methods = ["add", "add"]; - saveNetworkMetrics(keys, values, methods); - - // Update provider and proof set metrics - const weekId = event.block.timestamp.toI32() / 604800; - const monthId = event.block.timestamp.toI32() / 2592000; - const providerId = proofSet.owner; - const weeklyProviderId = Bytes.fromI32(weekId).concat(providerId); - const monthlyProviderId = Bytes.fromI32(monthId).concat(providerId); - const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); - const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); - saveProviderMetrics( - "WeeklyProviderActivity", - weeklyProviderId, - providerId, - ["totalFaultedPeriods", "totalFaultedRoots"], - [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], - ["add", "add"] - ); - saveProviderMetrics( - "MonthlyProviderActivity", - monthlyProviderId, - providerId, - ["totalFaultedPeriods", "totalFaultedRoots"], - [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], - ["add", "add"] - ); - saveProofSetMetrics( - "WeeklyProofSetActivity", - weeklyProofSetId, - setId, - ["totalFaultedPeriods", "totalFaultedRoots"], - [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], - ["add", "add"] - ); - saveProofSetMetrics( - "MonthlyProofSetActivity", - monthlyProofSetId, - setId, - ["totalFaultedPeriods", "totalFaultedRoots"], - [periodsFaultedParam, BigInt.fromI32(uniqueRootIds.length)], - ["add", "add"] - ); -} diff --git a/apps/subgraph/src/pdp-verifier.ts b/apps/subgraph/src/pdp-verifier.ts index 2d687c3b..b7c1547e 100644 --- a/apps/subgraph/src/pdp-verifier.ts +++ b/apps/subgraph/src/pdp-verifier.ts @@ -1,36 +1,21 @@ -import { BigInt, Bytes, log, store, Value } from "@graphprotocol/graph-ts"; +import { BigInt, Bytes, log } from "@graphprotocol/graph-ts"; import { - NextProvingPeriod as NextProvingPeriodEvent, - PossessionProven as PossessionProvenEvent, - ProofFeePaid as ProofFeePaidEvent, DataSetCreated as DataSetCreatedEvent, DataSetDeleted as DataSetDeletedEvent, DataSetEmpty as DataSetEmptyEvent, - StorageProviderChanged as StorageProviderChangedEvent, + NextProvingPeriod as NextProvingPeriodEvent, PiecesAdded as PiecesAddedEvent, PiecesRemoved as PiecesRemovedEvent, + PossessionProven as PossessionProvenEvent, + StorageProviderChanged as StorageProviderChangedEvent, } from "../generated/PDPVerifier/PDPVerifier"; -import { - EventLog, - Provider, - DataSet, - Root, - Transaction, - Service, - ServiceProviderLink, - ProvingWindow, -} from "../generated/schema"; -import { - saveProviderMetrics, - saveProofSetMetrics, - saveNetworkMetrics, -} from "./helper"; -import { SumTree } from "./sumTree"; -import { LeafSize, MaxProvingPeriod, ChallengeWindowSize, MaxProvingWindowsPerEvent } from "../utils"; -import { validateCommPv2, unpaddedSize } from "../utils/cid"; +import { DataSet, Provider, Root } from "../generated/schema"; import { DataSetStatus } from "./types"; +import { MaxProvingPeriod } from "../utils"; +import { unpaddedSize, validateCommPv2 } from "../utils/cid"; + +// ---- Entity ID helpers ---------------------------------------------------- -// --- Helper Functions for ID Generation --- function getProofSetEntityId(setId: BigInt): Bytes { return Bytes.fromByteArray(Bytes.fromBigInt(setId)); } @@ -39,1565 +24,210 @@ export function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); } -function getServiceProviderLinkEntityId( - serviceAddr: Bytes, - providerAddr: Bytes -): Bytes { - return serviceAddr.concat(providerAddr); -} - -function getTransactionEntityId(txHash: Bytes): Bytes { - return txHash; -} - -function getEventLogEntityId(txHash: Bytes, logIndex: BigInt): Bytes { - return txHash.concatI32(logIndex.toI32()); -} - -// ----------------------------------------- +// ---- Handlers ------------------------------------------------------------- export function handleDataSetCreated(event: DataSetCreatedEvent): void { - const listenerAddr = Bytes.fromUint8Array( - event.transaction.input.subarray(16, 36) - ); - const proofSetEntityId = getProofSetEntityId(event.params.setId); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const providerEntityId = event.params.storageProvider; // Provider ID is the owner address - - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = event.params.setId; // Keep raw ID for potential filtering - eventLog.address = event.address; - eventLog.name = "DataSetCreated"; - eventLog.data = `{"setId":"${event.params.setId.toString()}","storageProvider":"${event.params.storageProvider.toHexString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; // Keep raw hash - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); + const providerEntityId = event.params.storageProvider; - // Create Transaction - // Check if transaction already exists (e.g., from another log in the same tx) - let transaction = Transaction.load(transactionEntityId); - if (transaction == null) { - transaction = new Transaction(transactionEntityId); - transaction.hash = event.transaction.hash; - transaction.dataSetId = event.params.setId; // Keep raw ID for potential filtering - transaction.height = event.block.number; - transaction.fromAddress = event.transaction.from; - transaction.toAddress = event.transaction.to; // Can be null for contract creation - transaction.value = event.transaction.value; - transaction.method = "createDataSet"; // Or derive from input data if possible - transaction.status = true; // Assuming success if event emitted - transaction.createdAt = event.block.timestamp; - // Link entities - transaction.proofSet = proofSetEntityId; - transaction.save(); - } - - // Create or load DataSet. FWSS.dataSetCreated callback fires before - // PDPVerifier's own DataSetCreated event (see PDPVerifier._createDataSet: - // listener callback runs, THEN `emit DataSetCreated`). If the listener is - // FWSS, handleFwssDataSetCreated has already created a stub entity with - // FWSS-layer fields populated. Load to preserve those fields. + // FWSS.dataSetCreated fires BEFORE PDPVerifier's own DataSetCreated event + // (see PDPVerifier._createDataSet: listener callback runs, THEN `emit + // DataSetCreated`). If the listener is FWSS, handleFwssDataSetCreated has + // already created a stub entity with FWSS-layer fields populated. Load to + // preserve those fields. let proofSet = DataSet.load(proofSetEntityId); if (proofSet == null) { proofSet = new DataSet(proofSetEntityId); - // FWSS fields — defaulted on create; FWSS handler will overwrite if it fires later. - proofSet.metadataKeys = []; - proofSet.metadataValues = []; proofSet.withIPFSIndexing = false; - proofSet.withCDN = false; - // fwssProviderId, fwssPayer, fwssServiceProvider, fwssPdpRailId, - // pdpPaymentEndEpoch are nullable — no init needed. + // fwssPayer, fwssServiceProvider, pdpPaymentEndEpoch are nullable. } proofSet.setId = event.params.setId; - proofSet.owner = providerEntityId; // Link to Provider via owner address (which is Provider's ID) - proofSet.listener = listenerAddr; + proofSet.owner = providerEntityId; proofSet.isActive = true; proofSet.status = DataSetStatus.EMPTY; - proofSet.leafCount = BigInt.fromI32(0); - proofSet.challengeRange = BigInt.fromI32(0); - proofSet.lastProvenEpoch = BigInt.fromI32(0); - proofSet.nextChallengeEpoch = BigInt.fromI32(0); - // Initialize proving period tracking fields (will be set when first NextProvingPeriod is called) - proofSet.firstDeadline = BigInt.fromI32(0); - proofSet.maxProvingPeriod = BigInt.fromI32(0); - proofSet.challengeWindowSize = BigInt.fromI32(0); - proofSet.currentDeadlineCount = BigInt.fromI32(0); - proofSet.nextDeadline = BigInt.fromI32(0); + proofSet.nextDeadline = BigInt.zero(); + proofSet.maxProvingPeriod = BigInt.zero(); proofSet.provenThisPeriod = false; - // Existing fields - proofSet.totalRoots = BigInt.fromI32(0); - proofSet.nextPieceId = BigInt.fromI32(0); - proofSet.totalDataSize = BigInt.fromI32(0); - proofSet.totalFeePaid = BigInt.fromI32(0); - proofSet.totalFaultedPeriods = BigInt.fromI32(0); - proofSet.totalFaultedRoots = BigInt.fromI32(0); - proofSet.totalProofs = BigInt.fromI32(0); - proofSet.totalProvedRoots = BigInt.fromI32(0); - proofSet.totalTransactions = BigInt.fromI32(1); - proofSet.totalEventLogs = BigInt.fromI32(1); - proofSet.createdAt = event.block.timestamp; - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; proofSet.save(); - // network metrics variables - let network_totalProofSets = BigInt.fromI32(1); - let network_totalActiveProofSets = BigInt.fromI32(1); - let network_totalProviders = BigInt.fromI32(0); - let network_totalServices = BigInt.fromI32(0); - - // Create or Update Provider let provider = Provider.load(providerEntityId); if (provider == null) { provider = new Provider(providerEntityId); provider.address = event.params.storageProvider; - provider.totalRoots = BigInt.fromI32(0); - provider.totalProofSets = BigInt.fromI32(1); - provider.totalProvingPeriods = BigInt.fromI32(0); - provider.totalFaultedPeriods = BigInt.fromI32(0); - provider.totalFaultedRoots = BigInt.fromI32(0); - provider.totalDataSize = BigInt.fromI32(0); - provider.createdAt = event.block.timestamp; - provider.blockNumber = event.block.number; - - // update network metrics - network_totalProviders = BigInt.fromI32(1); - } else { - // Update timestamp/block even if exists - provider.totalProofSets = provider.totalProofSets.plus(BigInt.fromI32(1)); - provider.blockNumber = event.block.number; - } - // provider.proofSetIds = provider.proofSetIds.concat([event.params.setId]); // REMOVED - Handled by @derivedFrom - provider.updatedAt = event.block.timestamp; - provider.save(); - - // Store Service - let service = Service.load(listenerAddr); - if (service == null) { - service = new Service(listenerAddr); - service.address = listenerAddr; - service.totalProofSets = BigInt.fromI32(1); - service.totalProviders = BigInt.fromI32(0); - service.totalRoots = BigInt.fromI32(0); - service.totalDataSize = BigInt.fromI32(0); - service.totalFaultedPeriods = BigInt.fromI32(0); - service.totalFaultedRoots = BigInt.fromI32(0); - service.createdAt = event.block.timestamp; - - network_totalServices = BigInt.fromI32(1); - } else { - service.totalProofSets = service.totalProofSets.plus(BigInt.fromI32(1)); - } - service.updatedAt = event.block.timestamp; - - // Store ServiceProviderLink - let serviceProviderLink = ServiceProviderLink.load( - getServiceProviderLinkEntityId(listenerAddr, event.params.storageProvider) - ); - if (serviceProviderLink == null) { - serviceProviderLink = new ServiceProviderLink( - getServiceProviderLinkEntityId(listenerAddr, event.params.storageProvider) - ); - serviceProviderLink.totalProofSets = BigInt.fromI32(1); - serviceProviderLink.service = listenerAddr; - serviceProviderLink.provider = event.params.storageProvider; - - // update service stats - service.totalProviders = service.totalProviders.plus(BigInt.fromI32(1)); - } else { - serviceProviderLink.totalProofSets = - serviceProviderLink.totalProofSets.plus(BigInt.fromI32(1)); + provider.totalFaultedPeriods = BigInt.zero(); + provider.totalProvingPeriods = BigInt.zero(); + provider.save(); } - service.save(); - serviceProviderLink.save(); - - // update network metrics - saveNetworkMetrics( - [ - "totalProofSets", - "totalActiveProofSets", - "totalProviders", - "totalServices", - ], - [ - network_totalProofSets, - network_totalActiveProofSets, - network_totalProviders, - network_totalServices, - ], - ["add", "add", "add", "add"] - ); - - // update provider and proof set metrics - const weekId = event.block.timestamp.toI32() / 604800; - const monthId = event.block.timestamp.toI32() / 2592000; - const weeklyProviderId = Bytes.fromI32(weekId).concat(providerEntityId); - const monthlyProviderId = Bytes.fromI32(monthId).concat(providerEntityId); - saveProviderMetrics( - "WeeklyProviderActivity", - weeklyProviderId, - providerEntityId, - ["totalProofSetsCreated"], - [BigInt.fromI32(1)], - ["add"] - ); - saveProviderMetrics( - "MonthlyProviderActivity", - monthlyProviderId, - providerEntityId, - ["totalProofSetsCreated"], - [BigInt.fromI32(1)], - ["add"] - ); } export function handleDataSetDeleted(event: DataSetDeletedEvent): void { - saveNetworkMetrics( - ["totalActiveProofSets"], - [BigInt.fromI32(1)], - ["subtract"] - ); - const setId = event.params.setId; - const deletedLeafCount = event.params.deletedLeafCount; - - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "DataSetDeleted"; - eventLog.data = `{"setId":"${setId.toString()}","deletedLeafCount":"${deletedLeafCount.toString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Create Transaction if it doesn't exist - let transaction = Transaction.load(transactionEntityId); - if (transaction == null) { - transaction = new Transaction(transactionEntityId); - transaction.hash = event.transaction.hash; - transaction.dataSetId = setId; - transaction.height = event.block.number; - transaction.fromAddress = event.transaction.from; - transaction.toAddress = event.transaction.to; - transaction.value = event.transaction.value; - transaction.method = "deleteDataSet"; // Example method name - transaction.status = true; - transaction.createdAt = event.block.timestamp; - transaction.proofSet = proofSetEntityId; // Link to DataSet - transaction.save(); - } - - // Load DataSet - const proofSet = DataSet.load(proofSetEntityId); - if (!proofSet) { - log.warning("DataSetDeleted: DataSet {} not found", [setId.toString()]); + const proofSet = DataSet.load(getProofSetEntityId(event.params.setId)); + if (proofSet == null) { + log.warning("DataSetDeleted: DataSet {} not found", [event.params.setId.toString()]); return; } - const ownerAddress = proofSet.owner; - - // Load Provider (to update stats before changing owner) - const provider = Provider.load(ownerAddress); - if (provider) { - provider.totalDataSize = provider.totalDataSize.minus( - proofSet.totalDataSize - ); - if (provider.totalDataSize.lt(BigInt.fromI32(0))) { - provider.totalDataSize = BigInt.fromI32(0); - } - provider.totalProofSets = provider.totalProofSets.minus(BigInt.fromI32(1)); - provider.updatedAt = event.block.timestamp; - provider.blockNumber = event.block.number; - provider.save(); - } else { - log.warning("DataSetDeleted: Provider {} for DataSet {} not found", [ - ownerAddress.toHexString(), - setId.toString(), - ]); - } - - // Update DataSet proofSet.isActive = false; proofSet.status = DataSetStatus.DELETED; - proofSet.owner = Bytes.empty(); - proofSet.totalRoots = BigInt.fromI32(0); - proofSet.totalDataSize = BigInt.fromI32(0); - proofSet.nextChallengeEpoch = BigInt.fromI32(0); - proofSet.lastProvenEpoch = BigInt.fromI32(0); - proofSet.totalTransactions = proofSet.totalTransactions.plus( - BigInt.fromI32(1) - ); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; + proofSet.nextDeadline = BigInt.zero(); proofSet.save(); - - // Note: Pieces associated with this DataSet are not automatically removed or updated here. - // They still exist but are linked to an inactive DataSet. - // Consider if Pieces should be marked as inactive or removed in handlePiecesRemoved if needed. } -export function handleStorageProviderChanged( - event: StorageProviderChangedEvent -): void { - const setId = event.params.setId; - const oldStorageProvider = event.params.oldStorageProvider; - const newStorageProvider = event.params.newStorageProvider; - - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "StorageProviderChanged"; - eventLog.data = `{"setId":"${setId.toString()}","oldStorageProvider":"${oldStorageProvider.toHexString()}","newStorageProvider":"${newStorageProvider.toHexString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Create Transaction if it doesn't exist - let transaction = Transaction.load(transactionEntityId); - if (transaction == null) { - transaction = new Transaction(transactionEntityId); - transaction.hash = event.transaction.hash; - transaction.dataSetId = setId; - transaction.height = event.block.number; - transaction.fromAddress = event.transaction.from; - transaction.toAddress = event.transaction.to; - transaction.value = event.transaction.value; - transaction.method = "claimDataSetStorageProvider"; // Example method name - transaction.status = true; - transaction.createdAt = event.block.timestamp; - transaction.proofSet = proofSetEntityId; // Link to DataSet - transaction.save(); - } - - // Load DataSet - const proofSet = DataSet.load(proofSetEntityId); - if (!proofSet) { - log.warning("StorageProviderChanged: DataSet {} not found", [ - setId.toString(), - ]); +export function handleStorageProviderChanged(event: StorageProviderChangedEvent): void { + const proofSet = DataSet.load(getProofSetEntityId(event.params.setId)); + if (proofSet == null) { + log.warning("StorageProviderChanged: DataSet {} not found", [event.params.setId.toString()]); return; } - // Load Old Provider (if exists) - Just update timestamp, derived field handles removal - const oldProvider = Provider.load(oldStorageProvider); - if (oldProvider) { - oldProvider.totalProofSets = oldProvider.totalProofSets.minus( - BigInt.fromI32(1) - ); - oldProvider.updatedAt = event.block.timestamp; - oldProvider.blockNumber = event.block.number; - oldProvider.save(); - } else { - log.warning("StorageProviderChanged: Old Provider {} not found", [ - oldStorageProvider.toHexString(), - ]); - } - - // load old ServiceProvider link - check if totalProofSets > 1 or not - // if not delete entity else decrease totalProofSets - const oldServiceProviderLink = ServiceProviderLink.load( - getServiceProviderLinkEntityId(proofSet.listener, oldStorageProvider) - ); - if (oldServiceProviderLink) { - if (oldServiceProviderLink.totalProofSets.gt(BigInt.fromI32(1))) { - oldServiceProviderLink.totalProofSets = - oldServiceProviderLink.totalProofSets.minus(BigInt.fromI32(1)); - } else { - store.remove("ServiceProviderLink", oldServiceProviderLink.id.toString()); - } - oldServiceProviderLink.save(); - } - - // load new ServiceProvider link - let newServiceProviderLink = ServiceProviderLink.load( - getServiceProviderLinkEntityId(proofSet.listener, newStorageProvider) - ); - if (newServiceProviderLink) { - newServiceProviderLink.totalProofSets = - newServiceProviderLink.totalProofSets.plus(BigInt.fromI32(1)); - } else { - newServiceProviderLink = new ServiceProviderLink( - getServiceProviderLinkEntityId(proofSet.listener, newStorageProvider) - ); - newServiceProviderLink.totalProofSets = BigInt.fromI32(1); - newServiceProviderLink.service = proofSet.listener; - newServiceProviderLink.provider = newStorageProvider; - } - newServiceProviderLink.save(); - - // Load or Create New Provider - Just update timestamp/create, derived field handles addition - let newProvider = Provider.load(newStorageProvider); + const newProviderId = event.params.newStorageProvider; + let newProvider = Provider.load(newProviderId); if (newProvider == null) { - // update network metrics - saveNetworkMetrics(["totalProviders"], [BigInt.fromI32(1)], ["add"]); - newProvider = new Provider(newStorageProvider); - newProvider.address = newStorageProvider; - newProvider.totalRoots = BigInt.fromI32(0); - newProvider.totalFaultedPeriods = BigInt.fromI32(0); - newProvider.totalProvingPeriods = BigInt.fromI32(0); - newProvider.totalFaultedRoots = BigInt.fromI32(0); - newProvider.totalDataSize = BigInt.fromI32(0); - newProvider.totalProofSets = BigInt.fromI32(1); - newProvider.createdAt = event.block.timestamp; - newProvider.blockNumber = event.block.number; - } else { - newProvider.totalProofSets = newProvider.totalProofSets.plus( - BigInt.fromI32(1) - ); - newProvider.blockNumber = event.block.number; + newProvider = new Provider(newProviderId); + newProvider.address = newProviderId; + newProvider.totalFaultedPeriods = BigInt.zero(); + newProvider.totalProvingPeriods = BigInt.zero(); + newProvider.save(); } - newProvider.updatedAt = event.block.timestamp; - newProvider.save(); - // Update DataSet Owner (this updates the derived relationship on both old and new Provider) - proofSet.owner = newStorageProvider; // Set owner to the new provider's ID - proofSet.totalTransactions = proofSet.totalTransactions.plus( - BigInt.fromI32(1) - ); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; + proofSet.owner = newProviderId; proofSet.save(); } -export function handleProofFeePaid(event: ProofFeePaidEvent): void { - const setId = event.params.setId; - const fee = event.params.fee; - - // update network metrics - saveNetworkMetrics(["totalProofFeePaidInFil"], [fee], ["add"]); - - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; // Keep raw ID - eventLog.address = event.address; - eventLog.name = "ProofFeePaid"; - eventLog.data = `{"dataSetId":"${setId.toString()}","fee":"${fee.toString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Update DataSet total fee paid - const proofSet = DataSet.load(proofSetEntityId); - if (proofSet) { - proofSet.totalFeePaid = proofSet.totalFeePaid.plus(fee); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; - proofSet.save(); - } else { - log.warning("ProofFeePaid: DataSet {} not found", [setId.toString()]); - } -} - export function handleDataSetEmpty(event: DataSetEmptyEvent): void { - const setId = event.params.setId; - - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "DataSetDeleted"; - eventLog.data = `{"setId":"${setId.toString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Update DataSet - const proofSet = DataSet.load(proofSetEntityId); - if (proofSet) { - const oldTotalDataSize = proofSet.totalDataSize; // Store size before zeroing - - proofSet.status = DataSetStatus.EMPTY; - proofSet.nextChallengeEpoch = BigInt.fromI32(0); - proofSet.lastProvenEpoch = BigInt.fromI32(0); - proofSet.totalRoots = BigInt.fromI32(0); - proofSet.totalDataSize = BigInt.fromI32(0); - proofSet.leafCount = BigInt.fromI32(0); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; - proofSet.save(); - - // Update Provider's total data size - const provider = Provider.load(proofSet.owner); - if (provider) { - // Subtract the size this proof set had *before* it was zeroed - provider.totalDataSize = provider.totalDataSize.minus(oldTotalDataSize); - if (provider.totalDataSize.lt(BigInt.fromI32(0))) { - provider.totalDataSize = BigInt.fromI32(0); // Prevent negative size - } - provider.updatedAt = event.block.timestamp; - provider.blockNumber = event.block.number; - provider.save(); - } else { - // It's possible the provider was deleted or owner changed before this event - log.warning("DataSetDeleted: Provider {} for DataSet {} not found", [ - proofSet.owner.toHexString(), - setId.toString(), - ]); - } - } else { - log.warning("DataSetDeleted: DataSet {} not found", [setId.toString()]); + const proofSet = DataSet.load(getProofSetEntityId(event.params.setId)); + if (proofSet == null) { + log.warning("DataSetEmpty: DataSet {} not found", [event.params.setId.toString()]); + return; } - // Note: This event implies all roots are gone. Existing Root entities - // linked to this DataSet might need to be marked as removed or deleted - // depending on the desired data retention policy. This handler doesn't do that. - // Consider adding logic here or in handlePiecesRemoved if needed. + + proofSet.status = DataSetStatus.EMPTY; + // Zero nextDeadline so the next PiecesAdded + NextProvingPeriod round + // re-enters the first-init branch and promotes to PROVING again. + proofSet.nextDeadline = BigInt.zero(); + proofSet.maxProvingPeriod = BigInt.zero(); + proofSet.provenThisPeriod = false; + proofSet.save(); } export function handlePossessionProven(event: PossessionProvenEvent): void { - const setId = event.params.setId; - const challenges = event.params.challenges; // Array of { rootId: BigInt, offset: BigInt } - const currentBlockNumber = event.block.number; // Use block number as epoch indicator - const currentTimestamp = event.block.timestamp; - - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - // Create Event Log (Only one per event, log all challenges) - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "PossessionProven"; - // Store challenges as a simple string representation for the log - let challengesStr = "["; - for (let i = 0; i < challenges.length; i++) { - challengesStr += `{"pieceId":${challenges[i].pieceId.toString()},"offset":${challenges[i].offset.toString()}}`; - if (i < challenges.length - 1) { - challengesStr += ","; - } - } - challengesStr += "]"; - eventLog.data = `{"setId":"${setId.toString()}","challenges":${challengesStr}}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = currentTimestamp; - eventLog.blockNumber = currentBlockNumber; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Create Transaction (if it doesn't exist) - let transaction = Transaction.load(transactionEntityId); - if (transaction == null) { - transaction = new Transaction(transactionEntityId); - transaction.hash = event.transaction.hash; - transaction.dataSetId = setId; // Keep raw ID - transaction.height = currentBlockNumber; - transaction.fromAddress = event.transaction.from; - transaction.toAddress = event.transaction.to; - transaction.value = event.transaction.value; - transaction.method = "provePossession"; // Example method name - transaction.status = true; - transaction.createdAt = currentTimestamp; - transaction.proofSet = proofSetEntityId; // Link to DataSet - transaction.save(); - } - - let uniqueRoots: BigInt[] = []; - let pieceIdMap = new Map(); - - // Process each challenge - for (let i = 0; i < challenges.length; i++) { - const challenge = challenges[i]; - const pieceId = challenge.pieceId; - - const pieceIdStr = pieceId.toString(); - if (!pieceIdMap.has(pieceIdStr)) { - uniqueRoots.push(pieceId); - pieceIdMap.set(pieceIdStr, true); - } - } - - for (let i = 0; i < uniqueRoots.length; i++) { - const rootId = uniqueRoots[i]; - const rootEntityId = getRootEntityId(setId, rootId); - const root = Root.load(rootEntityId); - if (root) { - root.lastProvenEpoch = currentBlockNumber; - root.lastProvenAt = currentTimestamp; - root.totalProofsSubmitted = root.totalProofsSubmitted.plus( - BigInt.fromI32(1) - ); - root.updatedAt = currentTimestamp; - root.blockNumber = currentBlockNumber; - root.save(); - } else { - log.warning( - "PossessionProven: Root {} for Set {} not found during challenge processing", - [rootId.toString(), setId.toString()] - ); - } - } - - // Update DataSet (once per event) - const proofSet = DataSet.load(proofSetEntityId); - if (proofSet) { - const deadlineCount = proofSet.currentDeadlineCount; - - // Update existing proving window - const provingWindowId = Bytes.fromUTF8( - setId.toString() + "-" + deadlineCount.toString() - ); - const currentProvingWindow = ProvingWindow.load(provingWindowId); - - if (currentProvingWindow) { - currentProvingWindow.proofSubmitted = true; - currentProvingWindow.proofBlockNumber = currentBlockNumber; - currentProvingWindow.isValid = true; - currentProvingWindow.save(); - } else { - log.warning( - "PossessionProven: proving window not found for set {} and deadline count {}", - [setId.toString(), deadlineCount.toString()] - ); - } - - proofSet.lastProvenEpoch = currentBlockNumber; // Update last proven epoch for the set - proofSet.provenThisPeriod = true; // Mark that proof was submitted this period - proofSet.totalProvedRoots = proofSet.totalProvedRoots.plus( - BigInt.fromI32(uniqueRoots.length) - ); - proofSet.totalProofs = proofSet.totalProofs.plus(BigInt.fromI32(1)); - proofSet.totalTransactions = proofSet.totalTransactions.plus( - BigInt.fromI32(1) - ); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = currentTimestamp; - proofSet.blockNumber = currentBlockNumber; - proofSet.save(); - - // update provider and proof set metrics - const weekId = currentTimestamp.toI32() / 604800; - const monthId = currentTimestamp.toI32() / 2592000; - const providerAddr = proofSet.owner; - const weeklyProviderId = Bytes.fromI32(weekId).concat(providerAddr); - const monthlyProviderId = Bytes.fromI32(monthId).concat(providerAddr); - const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); - const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); - - saveProviderMetrics( - "WeeklyProviderActivity", - weeklyProviderId, - providerAddr, - ["totalProofs", "totalRootsProved"], - [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], - ["add", "add"] - ); - saveProviderMetrics( - "MonthlyProviderActivity", - monthlyProviderId, - providerAddr, - ["totalProofs", "totalRootsProved"], - [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], - ["add", "add"] - ); - saveProofSetMetrics( - "WeeklyProofSetActivity", - weeklyProofSetId, - setId, - ["totalProofs", "totalRootsProved"], - [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], - ["add", "add"] - ); - saveProofSetMetrics( - "MonthlyProofSetActivity", - monthlyProofSetId, - setId, - ["totalProofs", "totalRootsProved"], - [BigInt.fromI32(1), BigInt.fromI32(uniqueRoots.length)], - ["add", "add"] - ); - } else { - log.warning("PossessionProven: DataSet {} not found", [setId.toString()]); + const proofSet = DataSet.load(getProofSetEntityId(event.params.setId)); + if (proofSet == null) { + log.warning("PossessionProven: DataSet {} not found", [event.params.setId.toString()]); + return; } - // Update network metrics - saveNetworkMetrics( - ["totalProvedRoots", "totalProofs"], - [BigInt.fromI32(uniqueRoots.length), BigInt.fromI32(1)], - ["add", "add"] - ); + // Flip the flag so the next NextProvingPeriod classifies this period as + // proven rather than faulted. + proofSet.provenThisPeriod = true; + proofSet.save(); } export function handleNextProvingPeriod(event: NextProvingPeriodEvent): void { const setId = event.params.setId; - const challengeEpoch = event.params.challengeEpoch; - const leafCount = event.params.leafCount; - const currentTimestamp = event.block.timestamp; const currentBlockNumber = event.block.number; - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); - - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "NextProvingPeriod"; - eventLog.data = `{"setId":"${setId.toString()}","challengeEpoch":"${challengeEpoch.toString()}","leafCount":"${leafCount.toString()}"}`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = currentTimestamp; - eventLog.blockNumber = currentBlockNumber; - // Link entities - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Create Transaction (if it doesn't exist) - let transaction = Transaction.load(transactionEntityId); - if (transaction == null) { - transaction = new Transaction(transactionEntityId); - transaction.hash = event.transaction.hash; - transaction.dataSetId = setId; - transaction.height = event.block.number; - transaction.fromAddress = event.transaction.from; - transaction.toAddress = event.transaction.to; - transaction.value = event.transaction.value; - transaction.method = "nextProvingPeriod"; // Example method name - transaction.status = true; - transaction.createdAt = currentTimestamp; - transaction.proofSet = proofSetEntityId; // Link to DataSet - transaction.save(); + const proofSet = DataSet.load(getProofSetEntityId(setId)); + if (proofSet == null) { + log.warning("NextProvingPeriod: DataSet {} not found", [setId.toString()]); + return; } - // Update Data Set - const proofSet = DataSet.load(proofSetEntityId); - if (proofSet) { - let periodsSkipped: BigInt = BigInt.zero(); - let faultedPeriods: BigInt = BigInt.zero(); - let nextDeadline: BigInt; - - // initialize state for new data set - if (proofSet.nextDeadline.equals(BigInt.fromI32(0))) { - proofSet.status = DataSetStatus.PROVING; - proofSet.firstDeadline = currentBlockNumber; - // Set default values for proving period configuration. - // Modify MaxProvingPeriod / ChallengeWindowSize in utils/index.ts for each network. - proofSet.maxProvingPeriod = BigInt.fromI32(MaxProvingPeriod); - proofSet.challengeWindowSize = BigInt.fromI32(ChallengeWindowSize); - nextDeadline = currentBlockNumber.plus(proofSet.maxProvingPeriod); - } else { - if (currentBlockNumber.gt(proofSet.nextDeadline)) - periodsSkipped = currentBlockNumber - .minus(proofSet.nextDeadline.plus(BigInt.fromI32(1))) - .div(proofSet.maxProvingPeriod); - - nextDeadline = proofSet.nextDeadline.plus( - proofSet.maxProvingPeriod.times(periodsSkipped.plus(BigInt.fromI32(1))) - ); - faultedPeriods = proofSet.provenThisPeriod - ? periodsSkipped - : periodsSkipped.plus(BigInt.fromI32(1)); - } - - // Create ProvingWindow entity for skipped and current period. - // Cap the number of entities created per event to avoid OOM when syncing stale datasets - // that have accumulated many skipped periods (periodsSkipped can be tens of thousands). - // Fault counts are tracked accurately in faultedPeriods regardless of this cap. - const provingWindows = proofSet.leafCount.equals(BigInt.fromI32(0)) - ? periodsSkipped - : periodsSkipped.plus(BigInt.fromI32(1)); - const windowCap = BigInt.fromI32(MaxProvingWindowsPerEvent); - const windowStart = provingWindows.gt(windowCap) - ? provingWindows.minus(windowCap) - : BigInt.fromI32(0); - for (let i = windowStart.toI64(); i < provingWindows.toI64(); i++) { - const deadlineCount = proofSet.currentDeadlineCount - .plus(BigInt.fromI32(1)) - .plus(BigInt.fromI64(i)); - const periodDeadline = proofSet.firstDeadline.plus( - deadlineCount.times(proofSet.maxProvingPeriod) - ); - const provingWindowId = Bytes.fromUTF8( - setId.toString() + "-" + deadlineCount.toString() - ); - let provingWindow = new ProvingWindow(provingWindowId); - provingWindow.setId = setId; - provingWindow.deadlineCount = deadlineCount; - provingWindow.deadline = periodDeadline; - provingWindow.windowStart = periodDeadline.minus( - proofSet.challengeWindowSize - ); - provingWindow.windowEnd = periodDeadline; - provingWindow.proofSubmitted = false; - provingWindow.proofBlockNumber = BigInt.fromI32(0); - provingWindow.isValid = false; - provingWindow.createdAt = currentTimestamp; - provingWindow.proofSet = proofSetEntityId; - provingWindow.save(); - } - - proofSet.nextDeadline = nextDeadline; - proofSet.nextChallengeEpoch = challengeEpoch; - proofSet.challengeRange = leafCount; - proofSet.currentDeadlineCount = proofSet.currentDeadlineCount.plus( - periodsSkipped.plus(BigInt.fromI32(1)) - ); - proofSet.provenThisPeriod = false; - proofSet.totalFaultedPeriods = - proofSet.totalFaultedPeriods.plus(faultedPeriods); - proofSet.totalTransactions = proofSet.totalTransactions.plus( - BigInt.fromI32(1) - ); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = currentTimestamp; - proofSet.blockNumber = currentBlockNumber; + let periodsSkipped: BigInt = BigInt.zero(); + let faultedPeriods: BigInt = BigInt.zero(); + let nextDeadline: BigInt; - // Check if data set is empty - if (proofSet.leafCount.equals(BigInt.fromI32(0))) { - proofSet.nextDeadline = BigInt.fromI32(0); - proofSet.nextChallengeEpoch = BigInt.fromI32(0); - proofSet.firstDeadline = BigInt.fromI32(0); - proofSet.maxProvingPeriod = BigInt.fromI32(0); - proofSet.challengeWindowSize = BigInt.fromI32(0); - proofSet.currentDeadlineCount = BigInt.fromI32(0); - } - - proofSet.save(); - - const provider = Provider.load(proofSet.owner); - if (provider) { - provider.totalFaultedPeriods = - provider.totalFaultedPeriods.plus(faultedPeriods); - provider.totalProvingPeriods = provider.totalProvingPeriods.plus( - periodsSkipped.plus(BigInt.fromI32(1)) - ); - provider.updatedAt = currentTimestamp; - provider.blockNumber = currentBlockNumber; - provider.save(); + if (proofSet.nextDeadline.equals(BigInt.zero())) { + // First-init: promote to PROVING, seed maxProvingPeriod. + proofSet.status = DataSetStatus.PROVING; + proofSet.maxProvingPeriod = BigInt.fromI32(MaxProvingPeriod); + nextDeadline = currentBlockNumber.plus(proofSet.maxProvingPeriod); + } else { + if (currentBlockNumber.gt(proofSet.nextDeadline)) { + periodsSkipped = currentBlockNumber + .minus(proofSet.nextDeadline.plus(BigInt.fromI32(1))) + .div(proofSet.maxProvingPeriod); } - - // update provider and proof set metrics - const weekId = currentTimestamp.toI32() / 604800; - const monthId = currentTimestamp.toI32() / 2592000; - const providerAddr = proofSet.owner; - const weeklyProviderId = Bytes.fromI32(weekId).concat(providerAddr); - const monthlyProviderId = Bytes.fromI32(monthId).concat(providerAddr); - const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); - const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); - - // Five challenges are supposed to be proven by an SP in an proving period - // Assuming that each challenge is for a unique root - const faultedRoots = faultedPeriods.times(BigInt.fromI32(5)); - - saveProviderMetrics( - "WeeklyProviderActivity", - weeklyProviderId, - providerAddr, - ["totalFaultedPeriods", "totalFaultedRoots"], - [faultedPeriods, faultedRoots], - ["add", "add"] - ); - saveProviderMetrics( - "MonthlyProviderActivity", - monthlyProviderId, - providerAddr, - ["totalFaultedPeriods", "totalFaultedRoots"], - [faultedPeriods, faultedRoots], - ["add", "add"] - ); - saveProofSetMetrics( - "WeeklyProofSetActivity", - weeklyProofSetId, - setId, - ["totalFaultedPeriods", "totalFaultedRoots"], - [faultedPeriods, faultedRoots], - ["add", "add"] - ); - saveProofSetMetrics( - "MonthlyProofSetActivity", - monthlyProofSetId, - setId, - ["totalFaultedPeriods", "totalFaultedRoots"], - [faultedPeriods, faultedRoots], - ["add", "add"] + nextDeadline = proofSet.nextDeadline.plus( + proofSet.maxProvingPeriod.times(periodsSkipped.plus(BigInt.fromI32(1))), ); + faultedPeriods = proofSet.provenThisPeriod ? periodsSkipped : periodsSkipped.plus(BigInt.fromI32(1)); + } - // update network metrics - saveNetworkMetrics( - ["totalFaultedPeriods", "totalFaultedRoots"], - [faultedPeriods, faultedRoots], - ["add", "add"] - ); + proofSet.nextDeadline = nextDeadline; + proofSet.provenThisPeriod = false; + proofSet.save(); - // update Service Metrics - const service = Service.load(proofSet.listener); - if (service) { - service.totalFaultedPeriods = - service.totalFaultedPeriods.plus(faultedPeriods); - service.totalFaultedRoots = service.totalFaultedRoots.plus(faultedRoots); - service.updatedAt = currentTimestamp; - service.save(); - } - } else { - log.warning("NextProvingPeriod: DataSet {} not found", [setId.toString()]); + const provider = Provider.load(proofSet.owner); + if (provider != null) { + provider.totalFaultedPeriods = provider.totalFaultedPeriods.plus(faultedPeriods); + provider.totalProvingPeriods = provider.totalProvingPeriods.plus(periodsSkipped.plus(BigInt.fromI32(1))); + provider.save(); } } export function handlePiecesAdded(event: PiecesAddedEvent): void { const setId = event.params.setId; - const rootIdsFromEvent = event.params.pieceIds; // Get root IDs from event params + const rootIdsFromEvent = event.params.pieceIds; const pieceCidsFromEvent = event.params.pieceCids; - // Input parsing is necessary to get rawSize and root bytes (cid) - const txInput = event.transaction.input; - - if (txInput.length < 4) { - log.error("Invalid tx input length in handlePiecesAdded: {}", [ - event.transaction.hash.toHex(), - ]); + const proofSet = DataSet.load(getProofSetEntityId(setId)); + if (proofSet == null) { + log.warning("handlePiecesAdded: DataSet {} not found", [setId.toString()]); return; } - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); + let addedAny = false; - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "piecesAdded"; - // Store simple representation of event params - let pieceIdStrings: string[] = []; for (let i = 0; i < rootIdsFromEvent.length; i++) { - pieceIdStrings.push(rootIdsFromEvent[i].toString()); - } - eventLog.data = `{ "setId": "${setId.toString()}", "pieceIds": [${pieceIdStrings.join(",")}] }`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Create Transaction (if it doesn't exist) - let transaction = Transaction.load(transactionEntityId); - if (transaction == null) { - transaction = new Transaction(transactionEntityId); - transaction.hash = event.transaction.hash; - transaction.dataSetId = setId; - transaction.height = event.block.number; - transaction.fromAddress = event.transaction.from; - const toAddress = event.transaction.to; - if (toAddress) { - transaction.toAddress = toAddress; - } - transaction.value = event.transaction.value; - transaction.method = "addPieces"; // Example method name - transaction.status = true; - transaction.createdAt = event.block.timestamp; - transaction.proofSet = proofSetEntityId; - transaction.save(); - } - - // Load DataSet - const proofSet = DataSet.load(proofSetEntityId); - if (!proofSet) { - log.warning("handlePiecesAdded: DataSet {} not found for event tx {}", [ - setId.toString(), - event.transaction.hash.toHex(), - ]); - return; - } - - // --- Parse Transaction Input --- Requires helper functions - // Skip function selector (first 4 bytes) - const encodedData = Bytes.fromUint8Array(txInput.slice(4)); - - // Decode setId (uint256 at offset 0) - let decodedSetId: BigInt = readUint256(encodedData, 0); - if (decodedSetId != setId) { - log.warning( - "Decoded setId {} does not match event param {} in handlePiecesAdded. Tx: {}. Using event param.", - [ - decodedSetId.toString(), - setId.toString(), - event.transaction.hash.toHex(), - ] - ); - } - - // Decode rootsData (tuple[]) - let rootsDataOffset = readUint256(encodedData, 64).toI32(); // Offset is at byte 32 - let rootsDataLength: i32; - - if (rootsDataOffset < 0 || encodedData.length < rootsDataOffset + 32) { - log.error( - "handlePiecesAdded: Invalid rootsDataOffset {} or data length {} for reading rootsData length. Tx: {}", - [ - rootsDataOffset.toString(), - encodedData.length.toString(), - event.transaction.hash.toHex(), - ] - ); - return; - } - - rootsDataLength = readUint256(encodedData, rootsDataOffset).toI32(); // Length is at the offset - - if (rootsDataLength < 0) { - log.error( - "handlePiecesAdded: Invalid negative rootsDataLength {}. Tx: {}", - [rootsDataLength.toString(), event.transaction.hash.toHex()] - ); - return; - } - - // Check if number of roots from input matches event param - if (rootsDataLength != rootIdsFromEvent.length) { - log.error( - "handlePiecesAdded: Decoded roots count ({}) does not match event param count ({}). Tx: {}", - [ - rootsDataLength.toString(), - rootIdsFromEvent.length.toString(), - event.transaction.hash.toHex(), - ] - ); - // Decide how to proceed. For now, use the event length as the source of truth for iteration. - rootsDataLength = rootIdsFromEvent.length; - } - - let addedRootCount = 0; - let totalDataSizeAdded = BigInt.fromI32(0); - - // Create Root entities - const structsBaseOffset = rootsDataOffset + 32; // Start of struct offsets/data - - for (let i = 0; i < rootsDataLength; i++) { - const rootId = rootIdsFromEvent[i]; // Use rootId from event params + const rootId = rootIdsFromEvent[i]; const pieceCid = pieceCidsFromEvent[i]; - // Calculate offset for this struct's data - const structDataRelOffset = readUint256( - encodedData, - structsBaseOffset + i * 32 - ).toI32(); - const structDataAbsOffset = rootsDataOffset + 32 + structDataRelOffset; // Correct absolute offset - - // Check bounds for reading struct content (root offset + rawSize) - if ( - structDataAbsOffset < 0 || - encodedData.length < structDataAbsOffset + 64 - ) { - log.error( - "handlePiecesAdded: Encoded data too short or invalid offset for root struct content. Index: {}, Offset: {}, Len: {}. Tx: {}", - [ - i.toString(), - structDataAbsOffset.toString(), - encodedData.length.toString(), - event.transaction.hash.toHex(), - ] - ); - continue; // Skip this root - } - const pieceBytes = pieceCid.data; const commPData = validateCommPv2(pieceBytes); - const rawSize = commPData.isValid - ? unpaddedSize(commPData.padding, commPData.height) - : BigInt.zero(); + const rawSize = commPData.isValid ? unpaddedSize(commPData.padding, commPData.height) : BigInt.zero(); const rootEntityId = getRootEntityId(setId, rootId); - - let root = Root.load(rootEntityId); - if (root) { - log.warning( - "handlePiecesAdded: Root {} for Set {} already exists. This shouldn't happen. Skipping.", - [rootId.toString(), setId.toString()] - ); + if (Root.load(rootEntityId) != null) { + log.warning("handlePiecesAdded: Root {} for Set {} already exists; skipping", [ + rootId.toString(), + setId.toString(), + ]); continue; } - root = new Root(rootEntityId); - root.rootId = rootId; + const root = new Root(rootEntityId); root.setId = setId; - root.rawSize = rawSize; // Use correct field name - root.leafCount = rawSize.div(BigInt.fromI32(LeafSize)); - root.cid = pieceCid.data; // Use correct field name - root.removed = false; // Explicitly set removed to false - root.lastProvenEpoch = BigInt.fromI32(0); - root.lastProvenAt = BigInt.fromI32(0); - root.lastFaultedEpoch = BigInt.fromI32(0); - root.lastFaultedAt = BigInt.fromI32(0); - root.totalProofsSubmitted = BigInt.fromI32(0); - root.totalPeriodsFaulted = BigInt.fromI32(0); - root.createdAt = event.block.timestamp; - root.updatedAt = event.block.timestamp; - root.blockNumber = event.block.number; - root.proofSet = proofSetEntityId; // Link to DataSet - // FWSS fields — defaulted here; patched in FWSS handler if applicable. - root.metadataKeys = []; - root.metadataValues = []; - // ipfsRootCID is nullable — no init needed. - + root.rootId = rootId; + root.rawSize = rawSize; + root.cid = pieceBytes; + root.removed = false; + root.proofSet = getProofSetEntityId(setId); + // ipfsRootCID: patched in FWSS handler if applicable. root.save(); - // Update SumTree - const sumTree = new SumTree(); - sumTree.sumTreeAdd( - setId.toI32(), - rawSize.div(BigInt.fromI32(LeafSize)), - rootId.toI32() - ); - - addedRootCount += 1; - totalDataSizeAdded = totalDataSizeAdded.plus(rawSize); + addedAny = true; } - // Update DataSet stats - const previousDataSize = proofSet.totalDataSize; - if (previousDataSize.equals(BigInt.zero())) { - // First piece added, mark as ready for proving - // status will change to PROVING in nextProvingPeriod call + // First non-empty add transitions the DataSet to READY. NextProvingPeriod + // will then promote it to PROVING. + if (addedAny && proofSet.status == DataSetStatus.EMPTY) { proofSet.status = DataSetStatus.READY; + proofSet.save(); } - proofSet.totalRoots = proofSet.totalRoots.plus( - BigInt.fromI32(addedRootCount) - ); - proofSet.nextPieceId = proofSet.nextPieceId.plus( - BigInt.fromI32(addedRootCount) - ); - proofSet.totalDataSize = proofSet.totalDataSize.plus(totalDataSizeAdded); - proofSet.leafCount = proofSet.leafCount.plus( - totalDataSizeAdded.div(BigInt.fromI32(LeafSize)) - ); - proofSet.totalTransactions = proofSet.totalTransactions.plus( - BigInt.fromI32(1) - ); - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; - proofSet.save(); - - // Update Provider stats - const provider = Provider.load(proofSet.owner); - if (provider) { - provider.totalDataSize = provider.totalDataSize.plus(totalDataSizeAdded); - provider.totalRoots = provider.totalRoots.plus( - BigInt.fromI32(addedRootCount) - ); - provider.updatedAt = event.block.timestamp; - provider.blockNumber = event.block.number; - provider.save(); - } else { - log.warning("handlePiecesAdded: Provider {} for DataSet {} not found", [ - proofSet.owner.toHex(), - setId.toString(), - ]); - } - - // update Service stats - const service = Service.load(proofSet.listener); - if (service) { - service.totalRoots = service.totalRoots.plus( - BigInt.fromI32(addedRootCount) - ); - service.totalDataSize = service.totalDataSize.plus(totalDataSizeAdded); - service.updatedAt = event.block.number; - service.save(); - } - - // Update network metrics - saveNetworkMetrics( - ["totalRoots", "totalActiveRoots", "totalDataSize"], - [ - BigInt.fromI32(addedRootCount), - BigInt.fromI32(addedRootCount), - totalDataSizeAdded, - ], - ["add", "add", "add"] - ); - - // update provider and proof set metrics - const weekId = event.block.timestamp.toI32() / 604800; - const monthId = event.block.timestamp.toI32() / 2592000; - const providerId = proofSet.owner; - const weeklyProviderId = Bytes.fromI32(weekId).concat(providerId); - const monthlyProviderId = Bytes.fromI32(monthId).concat(providerId); - const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); - const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); - saveProviderMetrics( - "WeeklyProviderActivity", - weeklyProviderId, - providerId, - ["totalRootsAdded", "totalDataSizeAdded"], - [BigInt.fromI32(addedRootCount), totalDataSizeAdded], - ["add", "add"] - ); - saveProviderMetrics( - "MonthlyProviderActivity", - monthlyProviderId, - providerId, - ["totalRootsAdded", "totalDataSizeAdded"], - [BigInt.fromI32(addedRootCount), totalDataSizeAdded], - ["add", "add"] - ); - saveProofSetMetrics( - "WeeklyProofSetActivity", - weeklyProofSetId, - setId, - ["totalRootsAdded", "totalDataSizeAdded"], - [BigInt.fromI32(addedRootCount), totalDataSizeAdded], - ["add", "add"] - ); - saveProofSetMetrics( - "MonthlyProofSetActivity", - monthlyProofSetId, - setId, - ["totalRootsAdded", "totalDataSizeAdded"], - [BigInt.fromI32(addedRootCount), totalDataSizeAdded], - ["add", "add"] - ); } export function handlePiecesRemoved(event: PiecesRemovedEvent): void { const setId = event.params.setId; - const pieceIds = event.params.pieceIds; - - const proofSetEntityId = getProofSetEntityId(setId); - const eventLogEntityId = getEventLogEntityId( - event.transaction.hash, - event.logIndex - ); - const transactionEntityId = getTransactionEntityId(event.transaction.hash); + const rootIds = event.params.pieceIds; - // Create Event Log - const eventLog = new EventLog(eventLogEntityId); - eventLog.setId = setId; - eventLog.address = event.address; - eventLog.name = "PiecesRemoved"; - // Store simple representation of event params - let removedRootIdStrings: string[] = []; - for (let i = 0; i < pieceIds.length; i++) { - removedRootIdStrings.push(pieceIds[i].toString()); - } - eventLog.data = `{ "setId": "${setId.toString()}", "pieceIds": [${removedRootIdStrings.join(",")}] }`; - eventLog.logIndex = event.logIndex; - eventLog.transactionHash = event.transaction.hash; - eventLog.createdAt = event.block.timestamp; - eventLog.blockNumber = event.block.number; - eventLog.proofSet = proofSetEntityId; - eventLog.transaction = transactionEntityId; - eventLog.save(); - - // Load DataSet - const proofSet = DataSet.load(proofSetEntityId); - if (!proofSet) { - log.warning("handlePiecesRemoved: DataSet {} not found for event tx {}", [ - setId.toString(), - event.transaction.hash.toHex(), - ]); - return; - } - - let removedRootCount = 0; - let removedDataSize = BigInt.fromI32(0); - - // Mark Root entities as removed (soft delete) - for (let i = 0; i < pieceIds.length; i++) { - const rootId = pieceIds[i]; - const rootEntityId = getRootEntityId(setId, rootId); - - const root = Root.load(rootEntityId); - if (root) { - removedRootCount += 1; - removedDataSize = removedDataSize.plus(root.rawSize); // Use correct field name - - // Mark the Root entity as removed instead of deleting - root.removed = true; - root.updatedAt = event.block.timestamp; - root.blockNumber = event.block.number; - root.save(); - - // Update SumTree - const sumTree = new SumTree(); - sumTree.sumTreeRemove( - setId.toI32(), - proofSet.nextPieceId.toI32(), - rootId.toI32(), - root.rawSize.div(BigInt.fromI32(LeafSize)), - event.block.number - ); - } else { - log.warning( - "handlePiecesRemoved: Root {} for Set {} not found. Cannot remove.", - [rootId.toString(), setId.toString()] - ); - } - } - - // Update DataSet stats - proofSet.totalRoots = proofSet.totalRoots.minus( - BigInt.fromI32(removedRootCount) - ); // Use correct field name - proofSet.totalDataSize = proofSet.totalDataSize.minus(removedDataSize); - proofSet.leafCount = proofSet.leafCount.minus( - removedDataSize.div(BigInt.fromI32(LeafSize)) - ); - - // Ensure stats don't go negative - if (proofSet.totalRoots.lt(BigInt.fromI32(0))) { - // Use correct field name - log.warning( - "handlePiecesRemoved: DataSet {} rootCount went negative. Setting to 0.", - [setId.toString()] - ); - proofSet.totalRoots = BigInt.fromI32(0); // Use correct field name - } - if (proofSet.totalDataSize.lt(BigInt.fromI32(0))) { - log.warning( - "handlePiecesRemoved: DataSet {} totalDataSize went negative. Setting to 0.", - [setId.toString()] - ); - proofSet.totalDataSize = BigInt.fromI32(0); - } - if (proofSet.leafCount.lt(BigInt.fromI32(0))) { - log.warning( - "handlePiecesRemoved: DataSet {} leafCount went negative. Setting to 0.", - [setId.toString()] - ); - proofSet.leafCount = BigInt.fromI32(0); - } - proofSet.totalEventLogs = proofSet.totalEventLogs.plus(BigInt.fromI32(1)); - proofSet.updatedAt = event.block.timestamp; - proofSet.blockNumber = event.block.number; - proofSet.save(); - - // Update Provider stats - const provider = Provider.load(proofSet.owner); - if (provider) { - provider.totalDataSize = provider.totalDataSize.minus(removedDataSize); - // Ensure provider totalDataSize doesn't go negative - if (provider.totalDataSize.lt(BigInt.fromI32(0))) { - log.warning( - "handlePiecesRemoved: Provider {} totalDataSize went negative. Setting to 0.", - [proofSet.owner.toHex()] - ); - provider.totalDataSize = BigInt.fromI32(0); - } - provider.totalRoots = provider.totalRoots.minus( - BigInt.fromI32(removedRootCount) - ); - // Ensure provider totalRoots doesn't go negative - if (provider.totalRoots.lt(BigInt.fromI32(0))) { - log.warning( - "handlePiecesRemoved: Provider {} totalRoots went negative. Setting to 0.", - [proofSet.owner.toHex()] - ); - provider.totalRoots = BigInt.fromI32(0); - } - provider.updatedAt = event.block.timestamp; - provider.blockNumber = event.block.number; - provider.save(); - } else { - log.warning("handlePiecesRemoved: Provider {} for DataSet {} not found", [ - proofSet.owner.toHex(), - setId.toString(), - ]); - } - - // update Service stats - const service = Service.load(proofSet.listener); - if (service) { - service.totalRoots = service.totalRoots.minus( - BigInt.fromI32(removedRootCount) - ); - // ensure totalRoots doesn't go negative - if (service.totalRoots.lt(BigInt.fromI32(0))) { - log.warning( - "handlePiecesRemoved: Service {} totalRoots went negative. Setting to 0.", - [proofSet.listener.toHex()] - ); - service.totalRoots = BigInt.fromI32(0); - } - service.totalDataSize = service.totalDataSize.minus(removedDataSize); - // ensure totalDataSize doesn't go negative - if (service.totalDataSize.lt(BigInt.fromI32(0))) { - log.warning( - "handlePiecesRemoved: Service {} totalDataSize went negative. Setting to 0.", - [proofSet.listener.toHex()] - ); - service.totalDataSize = BigInt.fromI32(0); + for (let i = 0; i < rootIds.length; i++) { + const root = Root.load(getRootEntityId(setId, rootIds[i])); + if (root == null) { + log.warning("handlePiecesRemoved: Root {} for Set {} not found", [rootIds[i].toString(), setId.toString()]); + continue; } - service.updatedAt = event.block.number; - service.save(); - } - - // Update network metrics - saveNetworkMetrics( - ["totalActiveRoots", "totalDataSize"], - [BigInt.fromI32(removedRootCount), removedDataSize], - ["subtract", "subtract"] - ); - - // Update provider and proof set metrics - const weekId = event.block.timestamp.toI32() / 604800; - const monthId = event.block.timestamp.toI32() / 2592000; - const providerId = proofSet.owner; - const weeklyProviderId = Bytes.fromI32(weekId).concat(providerId); - const monthlyProviderId = Bytes.fromI32(monthId).concat(providerId); - const weeklyProofSetId = Bytes.fromI32(weekId).concat(proofSetEntityId); - const monthlyProofSetId = Bytes.fromI32(monthId).concat(proofSetEntityId); - saveProviderMetrics( - "WeeklyProviderActivity", - weeklyProviderId, - providerId, - ["totalRootsRemoved", "totalDataSizeRemoved"], - [BigInt.fromI32(removedRootCount), removedDataSize], - ["add", "add"] - ); - saveProviderMetrics( - "MonthlyProviderActivity", - monthlyProviderId, - providerId, - ["totalRootsRemoved", "totalDataSizeRemoved"], - [BigInt.fromI32(removedRootCount), removedDataSize], - ["add", "add"] - ); - saveProofSetMetrics( - "WeeklyProofSetActivity", - weeklyProofSetId, - setId, - ["totalRootsRemoved", "totalDataSizeRemoved"], - [BigInt.fromI32(removedRootCount), removedDataSize], - ["add", "add"] - ); - saveProofSetMetrics( - "MonthlyProofSetActivity", - monthlyProofSetId, - setId, - ["totalRootsRemoved", "totalDataSizeRemoved"], - [BigInt.fromI32(removedRootCount), removedDataSize], - ["add", "add"] - ); -} - -// Helper function to read Uint256 from Bytes at a specific offset -function readUint256(data: Bytes, offset: i32): BigInt { - if (offset < 0 || data.length < offset + 32) { - log.error( - "readUint256: Invalid offset {} or data length {} for reading Uint256", - [offset.toString(), data.length.toString()] - ); - return BigInt.zero(); - } - // Slice 32 bytes and convert to BigInt (assuming big-endian) - const slicedBytes = Bytes.fromUint8Array(data.slice(offset, offset + 32)); - // Ensure bytes are reversed for correct BigInt conversion if needed (depends on source endianness) - // AssemblyScript's BigInt.fromUnsignedBytes assumes little-endian by default, reverse for big-endian - const reversedBytesArray = slicedBytes.reverse(); // Returns Uint8Array - const reversedBytes = Bytes.fromUint8Array(reversedBytesArray); // Create Bytes object - return BigInt.fromUnsignedBytes(reversedBytes); -} - -// Helper function to read dynamic Bytes from ABI-encoded data -function readBytes(data: Bytes, offset: i32): Bytes { - // First, read the offset to the actual bytes data (uint256) - const bytesTupleOffset = readUint256(data, offset).toI32(); - - // Check if the bytes offset is valid - if (bytesTupleOffset < 0 || data.length < offset + bytesTupleOffset + 32) { - log.error( - "readBytes: Invalid offset {} or data length {} for reading bytes length", - [bytesTupleOffset.toString(), data.length.toString()] - ); - return Bytes.empty(); - } - - const bytesOffset = readUint256(data, offset + bytesTupleOffset).toI32(); - const bytesAbsOffset = offset + bytesTupleOffset + bytesOffset; - // Read the length of the bytes (uint256) - const bytesLength = readUint256(data, bytesAbsOffset).toI32(); - - // Check if the length is valid - if (bytesLength < 0 || data.length < bytesAbsOffset + 32 + bytesLength) { - log.error( - "readBytes: Invalid length {} or data length {} for reading bytes data", - [bytesLength.toString(), data.length.toString()] - ); - return Bytes.empty(); + root.removed = true; + root.save(); } - - // Slice the actual bytes - return Bytes.fromUint8Array( - data.slice(bytesAbsOffset + 32, bytesAbsOffset + 32 + bytesLength) - ); } diff --git a/apps/subgraph/src/sumTree.ts b/apps/subgraph/src/sumTree.ts deleted file mode 100644 index 29084324..00000000 --- a/apps/subgraph/src/sumTree.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { BigInt, Bytes } from "@graphprotocol/graph-ts"; -import { SumTreeCount } from "../generated/schema"; - -// Define a class for the structure instead of a type alias -class PieceIdAndOffset { - rootId: BigInt; - offset: BigInt; -} - -export class SumTree { - private getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { - return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); - } - - // Helper: Get sumTreeCounts[setId][index], default 0 - private getSum(setId: i32, index: i32, blockNumber: BigInt): BigInt { - const rootEntityId = this.getRootEntityId( - BigInt.fromI32(setId as i32), - BigInt.fromI32(index as i32) - ); - const sumTreeCount = SumTreeCount.load(rootEntityId); - if (!sumTreeCount) return BigInt.fromI32(0); - if (sumTreeCount.lastDecEpoch.equals(blockNumber)) { - return sumTreeCount.lastCount; - } - return sumTreeCount.count; - } - - // Helper: Set sumTreeCounts[setId][index] = value - private setSum(setId: i32, index: i32, value: BigInt): void { - const rootEntityId = this.getRootEntityId( - BigInt.fromI32(setId), - BigInt.fromI32(index) - ); - const sumTreeCount = new SumTreeCount(rootEntityId); - sumTreeCount.setId = BigInt.fromI32(setId as i32); - sumTreeCount.rootId = BigInt.fromI32(index as i32); - sumTreeCount.count = value; - sumTreeCount.lastCount = BigInt.fromI32(0); - sumTreeCount.lastDecEpoch = BigInt.fromI32(0); - sumTreeCount.save(); - } - - // Helper: Decrement sumTreeCounts[setId][index] by delta - private decSum( - setId: i32, - index: i32, - delta: BigInt, - blockNumber: BigInt - ): void { - const rootEntityId = this.getRootEntityId( - BigInt.fromI32(setId), - BigInt.fromI32(index) - ); - const sumTreeCount = SumTreeCount.load(rootEntityId); - if (!sumTreeCount) return; - const prev = sumTreeCount.count; - sumTreeCount.lastCount = prev; - sumTreeCount.count = prev.minus(delta); - sumTreeCount.lastDecEpoch = blockNumber; - sumTreeCount.save(); - } - - // Helper: heightFromIndex (number of trailing zeros in index+1) - private heightFromIndex(index: i32): i32 { - let x = index + 1; - let tz = 0; - while ((x & 1) === 0) { - tz++; - x >>= 1; - } - return tz; - } - - // Helper: clz (count leading zeros) for 32-bit numbers - private clz(x: i32): i32 { - if (x === 0) return 32; - let n = 32; - let y = (x as u32) >> 16; - if (y !== 0) { - n -= 16; - x = y; - } - y = (x as u32) >> 8; - if (y !== 0) { - n -= 8; - x = y; - } - y = (x as u32) >> 4; - if (y !== 0) { - n -= 4; - x = y; - } - y = (x as u32) >> 2; - if (y !== 0) { - n -= 2; - x = y; - } - y = (x as u32) >> 1; - if (y !== 0) { - return n - 2; - } - return n - (x as i32); - } - - // sumTreeAdd - sumTreeAdd(setId: i32, count: BigInt, rootId: i32): void { - let index = rootId; - let h = this.heightFromIndex(index); - let sum = count; - for (let i = 0; i < h; i++) { - let j = index - (1 << i); - sum = sum.plus(this.getSum(setId, j, BigInt.fromI32(1))); // 0 is default value of lastDecEpoch so using 1 - } - this.setSum(setId, rootId, sum); - } - - // sumTreeRemove - sumTreeRemove( - setId: i32, - nextRoot: i32, - index: i32, - delta: BigInt, - blockNumber: BigInt - ): void { - const top = 32 - this.clz(nextRoot); - let h = this.heightFromIndex(index); - while (h <= top && index < nextRoot) { - this.decSum(setId, index, delta, blockNumber); - index += 1 << h; - h = this.heightFromIndex(index); - } - } - - // findOneRootId - findOneRootId( - setId: i32, - nextRoot: i32, - leafIndex: BigInt, - top: i32, - blockNumber: BigInt - ): PieceIdAndOffset { - let searchPtr = (1 << top) - 1; - let acc: BigInt = BigInt.fromI32(0); - let candidate: BigInt = BigInt.fromI32(0); - for (let h = top; h > 0; h--) { - if (searchPtr >= nextRoot) { - searchPtr -= 1 << (h - 1); - continue; - } - const sum = this.getSum(setId, searchPtr, blockNumber); - candidate = acc.plus(sum); - if (candidate.le(leafIndex)) { - acc = acc.plus(sum); - searchPtr += 1 << (h - 1); - } else { - searchPtr -= 1 << (h - 1); - } - } - candidate = acc.plus(this.getSum(setId, searchPtr, blockNumber)); - if (candidate.le(leafIndex)) { - return { - rootId: BigInt.fromI32(searchPtr + 1), - offset: leafIndex.minus(candidate), - }; - } - return { - rootId: BigInt.fromI32(searchPtr), - offset: leafIndex.minus(acc), - }; - } - - // findPieceIds (batched) - findPieceIds( - setId: i32, - nextPieceId: i32, - leafIndexes: BigInt[], - blockNumber: BigInt - ): PieceIdAndOffset[] { - const top = 32 - this.clz(nextPieceId); - - const results: PieceIdAndOffset[] = []; - for (let i = 0; i < leafIndexes.length; i++) { - const idx = leafIndexes[i]; - - const result = this.findOneRootId( - setId, - nextPieceId, - idx, - top, - blockNumber - ); - results.push(result); - } - - return results; - } -} - -export default SumTree; diff --git a/apps/subgraph/subgraph.yaml b/apps/subgraph/subgraph.yaml index 0e440d0a..a2ce05f2 100644 --- a/apps/subgraph/subgraph.yaml +++ b/apps/subgraph/subgraph.yaml @@ -19,14 +19,6 @@ dataSources: - DataSet - Provider - Root - - SumTreeCount - - Service - - ServiceProviderLink - - EventLog - - Transaction - - Proof - - ProofFee - - FaultRecord abis: - name: PDPVerifier file: ./abis/PDPVerifier.json @@ -41,8 +33,6 @@ dataSources: handler: handlePiecesAdded - event: PiecesRemoved(indexed uint256,uint256[]) handler: handlePiecesRemoved - - event: ProofFeePaid(indexed uint256,uint256) - handler: handleProofFeePaid - event: DataSetEmpty(indexed uint256) handler: handleDataSetEmpty - event: PossessionProven(indexed uint256,(uint256,uint256)[]) diff --git a/apps/subgraph/utils/index.ts b/apps/subgraph/utils/index.ts index d38b6283..38153b9b 100644 --- a/apps/subgraph/utils/index.ts +++ b/apps/subgraph/utils/index.ts @@ -1,15 +1,6 @@ -export const PDPVerifierAddress = "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C"; - -export const NumChallenges = 5; - -export const LeafSize = 32; - -// Proving period configuration per network. -// calibration: MaxProvingPeriod=240 -// mainnet: MaxProvingPeriod=2880 +// Default proving period length in blocks. Applied when a DataSet enters +// its first NextProvingPeriod. The FWSS contract sets the real value +// on-chain; this is a conservative default for subgraph state init. +// calibration: MaxProvingPeriod = 240 +// mainnet: MaxProvingPeriod = 2880 export const MaxProvingPeriod = 240; -export const ChallengeWindowSize = 20; - -// Maximum ProvingWindow entities created per NextProvingPeriod event. -// Prevents OOM when a stale dataset resumes after many skipped periods. -export const MaxProvingWindowsPerEvent = 50; From fa30fd43f9bd20bf2842efdd9f7ca9b877152a23 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 09:01:49 +0200 Subject: [PATCH 05/19] test(subgraph): trim to surviving handlers, wire CI, update env docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete fault-calculation.test.ts (962 LOC of SumTree + per-piece fault math that's gone). - Gut pdp-verifier.test.ts down to a single DataSet+Provider+Root sanity test. - Rewrite dataset-status.test.ts as a focused EMPTY→READY→PROVING→ EMPTY→READY→PROVING lifecycle run plus per-transition checks. - Rewrite fwss.test.ts to drop assertions on deleted fields (fwssProviderId, metadataKeys/Values, withCDN, fwssPdpRailId) and add an end-to-end test that exercises the exact shape backing GET_FWSS_CANDIDATE_PIECES. - Add .github/workflows/subgraph.yml: build both networks, restore mainnet manifest, run matchstick. Runs only on apps/subgraph/** changes. - Update .env.example and docs/environment-variables.md to point PDP_SUBGRAPH_ENDPOINT at the dealbot-owned Goldsky slots. All 22 matchstick tests pass. Goldsky deploy remains a follow-up requiring credentials (see apps/subgraph/README.md). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/subgraph.yml | 47 + apps/backend/.env.example | 3 +- apps/subgraph/tests/dataset-status.test.ts | 544 ++-------- apps/subgraph/tests/fault-calculation.test.ts | 962 ------------------ apps/subgraph/tests/fwss.test.ts | 372 ++----- apps/subgraph/tests/pdp-verifier.test.ts | 153 +-- docs/environment-variables.md | 9 +- 7 files changed, 287 insertions(+), 1803 deletions(-) create mode 100644 .github/workflows/subgraph.yml delete mode 100644 apps/subgraph/tests/fault-calculation.test.ts diff --git a/.github/workflows/subgraph.yml b/.github/workflows/subgraph.yml new file mode 100644 index 00000000..1fe39343 --- /dev/null +++ b/.github/workflows/subgraph.yml @@ -0,0 +1,47 @@ +name: Subgraph + +on: + pull_request: + paths: + - 'apps/subgraph/**' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.github/workflows/subgraph.yml' + +jobs: + build-and-test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.15.2 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build subgraph (mainnet) + run: pnpm --filter @dealbot/subgraph build:mainnet + + - name: Build subgraph (calibnet) + run: pnpm --filter @dealbot/subgraph build:calibnet + + - name: Restore mainnet manifest + # graph build --network X rewrites subgraph.yaml in place. Re-run the + # mainnet build to leave the checked-in file with mainnet defaults. + run: pnpm --filter @dealbot/subgraph build:mainnet + + - name: Run matchstick tests + run: pnpm --filter @dealbot/subgraph test diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 416b5cd5..dc1f3500 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -23,7 +23,8 @@ WALLET_ADDRESS=0x0000000000000000000000000000000000000000 WALLET_PRIVATE_KEY=your_private_key_here CHECK_DATASET_CREATION_FEES=true USE_ONLY_APPROVED_PROVIDERS=true -PDP_SUBGRAPH_ENDPOINT=https://api.thegraph.com/subgraphs/filecoin/pdp +# Point at the dealbot-owned subgraph on Goldsky (see apps/subgraph/README.md). +PDP_SUBGRAPH_ENDPOINT=https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn # Minimum number of datasets per SP (default: 1). When > 1, a separate data_set_creation job provisions extra datasets. MIN_NUM_DATASETS_FOR_CHECKS=1 diff --git a/apps/subgraph/tests/dataset-status.test.ts b/apps/subgraph/tests/dataset-status.test.ts index 8053f545..9dea8c4b 100644 --- a/apps/subgraph/tests/dataset-status.test.ts +++ b/apps/subgraph/tests/dataset-status.test.ts @@ -1,548 +1,204 @@ -import { - assert, - describe, - test, - clearStore, - afterEach, -} from "matchstick-as/assembly/index"; -import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; +import { afterEach, assert, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; import { handleDataSetCreated, - handlePiecesAdded, - handleNextProvingPeriod, handleDataSetDeleted, handleDataSetEmpty, + handleNextProvingPeriod, + handlePiecesAdded, } from "../src/pdp-verifier"; import { createDataSetCreatedEvent, - createRootsAddedEvent, - createNextProvingPeriodEvent, createDataSetDeletedEvent, createDataSetEmptyEvent, + createNextProvingPeriodEvent, + createRootsAddedEvent, generateTxHash, } from "./pdp-verifier-utils"; const SET_ID = BigInt.fromI32(1); const ROOT_ID_1 = BigInt.fromI32(101); -const SENDER_ADDRESS = Address.fromString( - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" -); -const CONTRACT_ADDRESS = Address.fromString( - "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" -); +const SENDER_ADDRESS = Address.fromString("0xa16081f360e3847006db660bae1c6d1b2e17ec2a"); +const CONTRACT_ADDRESS = Address.fromString("0xb16081f360e3847006db660bae1c6d1b2e17ec2b"); const PROOF_SET_ID_BYTES = Bytes.fromBigInt(SET_ID); +function createAndSubmitDataSet(txId: i32): void { + const event = createDataSetCreatedEvent( + SET_ID, + SENDER_ADDRESS, + Bytes.fromI32(123), + CONTRACT_ADDRESS, + BigInt.fromI32(100), + BigInt.fromI32(1678886400), + generateTxHash(txId), + BigInt.fromI32(0), + ); + handleDataSetCreated(event); +} + +function addRoots(txId: i32, blockNumber: i32): void { + const rootsEvent = createRootsAddedEvent(SET_ID, [ROOT_ID_1], SENDER_ADDRESS, CONTRACT_ADDRESS); + rootsEvent.block.timestamp = BigInt.fromI32(1678886500); + rootsEvent.block.number = BigInt.fromI32(blockNumber); + rootsEvent.logIndex = BigInt.fromI32(1); + rootsEvent.transaction.hash = generateTxHash(txId); + handlePiecesAdded(rootsEvent); +} + +function nextProvingPeriod(txId: i32, blockNumber: i32): void { + const event = createNextProvingPeriodEvent( + SET_ID, + BigInt.fromI32(blockNumber), + BigInt.fromI32(32), + CONTRACT_ADDRESS, + BigInt.fromI32(blockNumber), + BigInt.fromI32(1678886600), + generateTxHash(txId), + BigInt.fromI32(0), + ); + handleNextProvingPeriod(event); +} + describe("DataSetStatus Lifecycle Tests", () => { afterEach(() => { clearStore(); }); test("handleDataSetCreated sets status to EMPTY", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(1), - BigInt.fromI32(0) - ); + createAndSubmitDataSet(1); - handleDataSetCreated(mockDataSetCreatedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); - assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); }); test("handlePiecesAdded transitions status from EMPTY to READY", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(10), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); - - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent.block.number = BigInt.fromI32(150); - rootsAddedEvent.logIndex = BigInt.fromI32(1); - rootsAddedEvent.transaction.hash = generateTxHash(11); - - handlePiecesAdded(rootsAddedEvent); + createAndSubmitDataSet(10); + addRoots(11, 150); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "READY"); assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); }); test("handleNextProvingPeriod transitions status from READY to PROVING", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(20), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent.block.number = BigInt.fromI32(150); - rootsAddedEvent.logIndex = BigInt.fromI32(1); - rootsAddedEvent.transaction.hash = generateTxHash(21); - handlePiecesAdded(rootsAddedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "READY"); - - let nextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - BigInt.fromI32(200), - BigInt.fromI32(32), - CONTRACT_ADDRESS, - BigInt.fromI32(200), - BigInt.fromI32(1678886600), - generateTxHash(22), - BigInt.fromI32(0) - ); - - handleNextProvingPeriod(nextProvingPeriodEvent); + createAndSubmitDataSet(20); + addRoots(21, 150); + nextProvingPeriod(22, 200); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); - assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "200"); + assert.fieldEquals("DataSet", dataSetId, "maxProvingPeriod", "240"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "440"); // 200 + 240 }); test("handleDataSetDeleted transitions status to DELETED", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(30), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent.block.number = BigInt.fromI32(150); - rootsAddedEvent.logIndex = BigInt.fromI32(1); - rootsAddedEvent.transaction.hash = generateTxHash(31); - handlePiecesAdded(rootsAddedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + createAndSubmitDataSet(30); + addRoots(31, 150); - let dataSetDeletedEvent = createDataSetDeletedEvent( + const dataSetDeletedEvent = createDataSetDeletedEvent( SET_ID, BigInt.fromI32(32), CONTRACT_ADDRESS, BigInt.fromI32(200), BigInt.fromI32(1678886700), generateTxHash(32), - BigInt.fromI32(0) + BigInt.fromI32(0), ); - handleDataSetDeleted(dataSetDeletedEvent); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "DELETED"); assert.fieldEquals("DataSet", dataSetId, "isActive", "false"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); - assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); }); test("handleDataSetEmpty transitions status to EMPTY", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(40), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); + createAndSubmitDataSet(40); + addRoots(41, 150); - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent.block.number = BigInt.fromI32(150); - rootsAddedEvent.logIndex = BigInt.fromI32(1); - rootsAddedEvent.transaction.hash = generateTxHash(41); - handlePiecesAdded(rootsAddedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "READY"); - - let dataSetEmptyEvent = createDataSetEmptyEvent( + const dataSetEmptyEvent = createDataSetEmptyEvent( SET_ID, CONTRACT_ADDRESS, BigInt.fromI32(200), BigInt.fromI32(1678886700), generateTxHash(42), - BigInt.fromI32(0) + BigInt.fromI32(0), ); - handleDataSetEmpty(dataSetEmptyEvent); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); - assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); - assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); + assert.fieldEquals("DataSet", dataSetId, "maxProvingPeriod", "0"); }); test("handleDataSetDeleted from PROVING status transitions to DELETED", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(50), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); + createAndSubmitDataSet(50); + addRoots(51, 150); + nextProvingPeriod(52, 200); - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent.block.number = BigInt.fromI32(150); - rootsAddedEvent.logIndex = BigInt.fromI32(1); - rootsAddedEvent.transaction.hash = generateTxHash(51); - handlePiecesAdded(rootsAddedEvent); - - let nextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - BigInt.fromI32(200), - BigInt.fromI32(32), - CONTRACT_ADDRESS, - BigInt.fromI32(200), - BigInt.fromI32(1678886600), - generateTxHash(52), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(nextProvingPeriodEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); - let dataSetDeletedEvent = createDataSetDeletedEvent( + const dataSetDeletedEvent = createDataSetDeletedEvent( SET_ID, BigInt.fromI32(32), CONTRACT_ADDRESS, BigInt.fromI32(250), BigInt.fromI32(1678886800), generateTxHash(53), - BigInt.fromI32(0) + BigInt.fromI32(0), ); - handleDataSetDeleted(dataSetDeletedEvent); assert.fieldEquals("DataSet", dataSetId, "status", "DELETED"); assert.fieldEquals("DataSet", dataSetId, "isActive", "false"); }); - test("handleDataSetEmpty from PROVING status transitions to EMPTY", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(60), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent.block.number = BigInt.fromI32(150); - rootsAddedEvent.logIndex = BigInt.fromI32(1); - rootsAddedEvent.transaction.hash = generateTxHash(61); - handlePiecesAdded(rootsAddedEvent); - - let nextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - BigInt.fromI32(200), - BigInt.fromI32(32), - CONTRACT_ADDRESS, - BigInt.fromI32(200), - BigInt.fromI32(1678886600), - generateTxHash(62), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(nextProvingPeriodEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); - - let dataSetEmptyEvent = createDataSetEmptyEvent( - SET_ID, - CONTRACT_ADDRESS, - BigInt.fromI32(250), - BigInt.fromI32(1678886800), - generateTxHash(63), - BigInt.fromI32(0) - ); - - handleDataSetEmpty(dataSetEmptyEvent); - - assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); - assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); - }); - - test("Status remains EMPTY when no pieces are added", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(70), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - + test("Lifecycle: EMPTY → READY → PROVING → EMPTY → READY → PROVING", () => { + // 1. Create (EMPTY) + createAndSubmitDataSet(90); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); - assert.fieldEquals("DataSet", dataSetId, "totalDataSize", "0"); - assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); - }); - - test("Multiple pieces added keeps status as READY", () => { - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(80), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let pieceIds1 = [ROOT_ID_1]; - let rootsAddedEvent1 = createRootsAddedEvent( - SET_ID, - pieceIds1, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent1.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent1.block.number = BigInt.fromI32(150); - rootsAddedEvent1.logIndex = BigInt.fromI32(1); - rootsAddedEvent1.transaction.hash = generateTxHash(81); - handlePiecesAdded(rootsAddedEvent1); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "READY"); - - let pieceIds2 = [BigInt.fromI32(102)]; - let rootsAddedEvent2 = createRootsAddedEvent( - SET_ID, - pieceIds2, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent2.block.timestamp = BigInt.fromI32(1678886600); - rootsAddedEvent2.block.number = BigInt.fromI32(160); - rootsAddedEvent2.logIndex = BigInt.fromI32(1); - rootsAddedEvent2.transaction.hash = generateTxHash(82); - handlePiecesAdded(rootsAddedEvent2); - - assert.fieldEquals("DataSet", dataSetId, "status", "READY"); - }); - - test("Lifecycle: Add roots → Empty → Add roots again (with event sequence)", () => { - // Step 1: Create dataset (status = EMPTY) - let mockDataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - SENDER_ADDRESS, - Bytes.fromI32(123), - CONTRACT_ADDRESS, - BigInt.fromI32(100), - BigInt.fromI32(1678886400), - generateTxHash(90), - BigInt.fromI32(0) - ); - handleDataSetCreated(mockDataSetCreatedEvent); - - let dataSetId = PROOF_SET_ID_BYTES.toHex(); - assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); - assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); - assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "0"); assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); - // Step 2: Add roots (status = EMPTY → READY) - let pieceIds1 = [ROOT_ID_1]; - let rootsAddedEvent1 = createRootsAddedEvent( - SET_ID, - pieceIds1, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent1.block.timestamp = BigInt.fromI32(1678886500); - rootsAddedEvent1.block.number = BigInt.fromI32(150); - rootsAddedEvent1.logIndex = BigInt.fromI32(1); - rootsAddedEvent1.transaction.hash = generateTxHash(91); - handlePiecesAdded(rootsAddedEvent1); - + // 2. Add roots (READY) + addRoots(91, 150); assert.fieldEquals("DataSet", dataSetId, "status", "READY"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "1"); - assert.fieldEquals("DataSet", dataSetId, "leafCount", "327715"); - - // Step 3: NextProvingPeriod (status = READY → PROVING) - let nextProvingPeriodEvent1 = createNextProvingPeriodEvent( - SET_ID, - BigInt.fromI32(1), - BigInt.fromI32(327715), - CONTRACT_ADDRESS - ); - nextProvingPeriodEvent1.block.timestamp = BigInt.fromI32(1678886600); - nextProvingPeriodEvent1.block.number = BigInt.fromI32(200); - nextProvingPeriodEvent1.logIndex = BigInt.fromI32(1); - nextProvingPeriodEvent1.transaction.hash = generateTxHash(92); - handleNextProvingPeriod(nextProvingPeriodEvent1); + // 3. NextProvingPeriod (PROVING) + nextProvingPeriod(92, 200); assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); - assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "200"); - assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "440"); // 200 + 240 - assert.fieldEquals("DataSet", dataSetId, "currentDeadlineCount", "1"); - - // Step 4: Dataset becomes empty (PiecesRemoved → DataSetEmpty → NextProvingPeriod in same tx) - // Simulate the event sequence from contract's nextProvingPeriod function - - // Event 1: DataSetEmpty (emitted by contract) - let dataSetEmptyEvent = createDataSetEmptyEvent(SET_ID, CONTRACT_ADDRESS); - dataSetEmptyEvent.block.timestamp = BigInt.fromI32(1678886700); - dataSetEmptyEvent.block.number = BigInt.fromI32(250); - dataSetEmptyEvent.logIndex = BigInt.fromI32(1); - dataSetEmptyEvent.transaction.hash = generateTxHash(93); - handleDataSetEmpty(dataSetEmptyEvent); + assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "440"); - assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "0"); - assert.fieldEquals("DataSet", dataSetId, "leafCount", "0"); - assert.fieldEquals("DataSet", dataSetId, "nextChallengeEpoch", "0"); - assert.fieldEquals("DataSet", dataSetId, "lastProvenEpoch", "0"); - - // Event 2: NextProvingPeriod (same transaction, should handle empty dataset) - let nextProvingPeriodEvent2 = createNextProvingPeriodEvent( + // 4. DataSetEmpty (EMPTY) — resets deadline to 0 so next NPP re-seeds. + const dataSetEmptyEvent = createDataSetEmptyEvent( SET_ID, - BigInt.fromI32(2), + CONTRACT_ADDRESS, + BigInt.fromI32(250), + BigInt.fromI32(1678886700), + generateTxHash(93), BigInt.fromI32(0), - CONTRACT_ADDRESS ); - nextProvingPeriodEvent2.block.timestamp = BigInt.fromI32(1678886700); - nextProvingPeriodEvent2.block.number = BigInt.fromI32(250); - nextProvingPeriodEvent2.logIndex = BigInt.fromI32(2); - nextProvingPeriodEvent2.transaction.hash = generateTxHash(93); // Same tx hash - handleNextProvingPeriod(nextProvingPeriodEvent2); - - // Verify dataset remains EMPTY and proving fields are reset + handleDataSetEmpty(dataSetEmptyEvent); assert.fieldEquals("DataSet", dataSetId, "status", "EMPTY"); assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "0"); - assert.fieldEquals("DataSet", dataSetId, "nextChallengeEpoch", "0"); - assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "0"); - assert.fieldEquals("DataSet", dataSetId, "maxProvingPeriod", "0"); - assert.fieldEquals("DataSet", dataSetId, "challengeWindowSize", "0"); - assert.fieldEquals("DataSet", dataSetId, "currentDeadlineCount", "0"); - assert.fieldEquals("DataSet", dataSetId, "totalFaultedPeriods", "1"); - - // Step 5: Add roots again (status = EMPTY → READY) - let pieceIds2 = [BigInt.fromI32(201)]; - let rootsAddedEvent2 = createRootsAddedEvent( - SET_ID, - pieceIds2, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - rootsAddedEvent2.block.timestamp = BigInt.fromI32(1678886800); - rootsAddedEvent2.block.number = BigInt.fromI32(300); - rootsAddedEvent2.logIndex = BigInt.fromI32(1); - rootsAddedEvent2.transaction.hash = generateTxHash(94); - handlePiecesAdded(rootsAddedEvent2); + // 5. Add roots again (READY) + const rootsEvent = createRootsAddedEvent(SET_ID, [BigInt.fromI32(201)], SENDER_ADDRESS, CONTRACT_ADDRESS); + rootsEvent.block.number = BigInt.fromI32(300); + rootsEvent.logIndex = BigInt.fromI32(1); + rootsEvent.transaction.hash = generateTxHash(94); + handlePiecesAdded(rootsEvent); assert.fieldEquals("DataSet", dataSetId, "status", "READY"); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "1"); - assert.fieldEquals("DataSet", dataSetId, "leafCount", "327715"); - - // Step 6: NextProvingPeriod again (status = READY → PROVING with new firstDeadline) - let nextProvingPeriodEvent3 = createNextProvingPeriodEvent( - SET_ID, - BigInt.fromI32(1), // Challenge epoch resets - BigInt.fromI32(327715), - CONTRACT_ADDRESS - ); - nextProvingPeriodEvent3.block.timestamp = BigInt.fromI32(1678886900); - nextProvingPeriodEvent3.block.number = BigInt.fromI32(350); - nextProvingPeriodEvent3.logIndex = BigInt.fromI32(1); - nextProvingPeriodEvent3.transaction.hash = generateTxHash(95); - handleNextProvingPeriod(nextProvingPeriodEvent3); + // 6. NextProvingPeriod (PROVING) — first-init branch runs again since nextDeadline was zeroed. + nextProvingPeriod(95, 350); assert.fieldEquals("DataSet", dataSetId, "status", "PROVING"); - assert.fieldEquals("DataSet", dataSetId, "firstDeadline", "350"); // new firstDeadline, not 200 assert.fieldEquals("DataSet", dataSetId, "nextDeadline", "590"); // 350 + 240 - assert.fieldEquals("DataSet", dataSetId, "currentDeadlineCount", "1"); // Resets to 1 assert.fieldEquals("DataSet", dataSetId, "maxProvingPeriod", "240"); - assert.fieldEquals("DataSet", dataSetId, "challengeWindowSize", "20"); }); }); diff --git a/apps/subgraph/tests/fault-calculation.test.ts b/apps/subgraph/tests/fault-calculation.test.ts deleted file mode 100644 index 14c3cc6a..00000000 --- a/apps/subgraph/tests/fault-calculation.test.ts +++ /dev/null @@ -1,962 +0,0 @@ -import { - assert, - describe, - test, - clearStore, - beforeEach, - afterEach, -} from "matchstick-as/assembly/index"; -import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; -import { - handleDataSetCreated, - handleNextProvingPeriod, - handlePiecesAdded, - handlePossessionProven, -} from "../src/pdp-verifier"; -import { - createDataSetCreatedEvent, - createNextProvingPeriodEvent, - createPossessionProvenEvent, - createRootsAddedEvent, - generateTxHash, -} from "./pdp-verifier-utils"; - -const SET_ID = BigInt.fromI32(1); -const ROOT_ID_1 = BigInt.fromI32(101); -const PROVIDER_ADDRESS = Address.fromString( - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" -); -const CONTRACT_ADDRESS = Address.fromString( - "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" -); -const LISTENER_ADDRESS = Address.fromString( - "0x0000000000000000000000000000000000000001" -); -const MAX_PROVING_PERIOD = BigInt.fromI32(240); -const CHALLENGE_WINDOW_SIZE = BigInt.fromI32(20); -const SENDER_ADDRESS = Address.fromString( - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" -); - -function getProofSetId(): string { - return Bytes.fromBigInt(SET_ID).toHex(); -} - -function getProviderId(): string { - return PROVIDER_ADDRESS.toHex(); -} - -function addRootToDataSet(setId: BigInt, rootId: BigInt): void { - const rootsAddedEvent = createRootsAddedEvent( - setId, - [rootId], - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - - // Set block/tx details on the mock event if needed by handler - rootsAddedEvent.block.timestamp = BigInt.fromI32(100); // Example timestamp - rootsAddedEvent.block.number = BigInt.fromI32(50); // Example block number - rootsAddedEvent.logIndex = BigInt.fromI32(1); // Example log index - rootsAddedEvent.transaction.hash = Bytes.fromHexString("0x" + "c".repeat(64)); - - handlePiecesAdded(rootsAddedEvent); -} - -describe("Fault Calculation Tests", () => { - beforeEach(() => { - clearStore(); - }); - - afterEach(() => { - clearStore(); - }); - - test("Test 1: DataSet creation initializes with zero values", () => { - const blockNumber = BigInt.fromI32(100); - const timestamp = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - blockNumber, - timestamp, - generateTxHash(100), - BigInt.fromI32(0) - ); - - handleDataSetCreated(dataSetCreatedEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - assert.entityCount("DataSet", 1); - assert.entityCount("Provider", 1); - - assert.fieldEquals("DataSet", proofSetId, "setId", SET_ID.toString()); - assert.fieldEquals("DataSet", proofSetId, "nextDeadline", "0"); - assert.fieldEquals("DataSet", proofSetId, "firstDeadline", "0"); - assert.fieldEquals("DataSet", proofSetId, "maxProvingPeriod", "0"); - assert.fieldEquals("DataSet", proofSetId, "challengeWindowSize", "0"); - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "0"); - assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); - assert.fieldEquals("DataSet", proofSetId, "totalFaultedPeriods", "0"); - - assert.fieldEquals("Provider", providerId, "totalFaultedPeriods", "0"); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "0"); - }); - - test("Test 2: First nextProvingPeriod call sets initial deadline", () => { - const createBlockNumber = BigInt.fromI32(100); - const createTimestamp = BigInt.fromI32(1000); - const firstProvingBlockNumber = BigInt.fromI32(150); - const firstProvingTimestamp = BigInt.fromI32(1500); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - createTimestamp, - generateTxHash(200), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const nextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - firstProvingTimestamp, - generateTxHash(201), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(nextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - const expectedNextDeadline = - firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - - assert.fieldEquals( - "DataSet", - proofSetId, - "firstDeadline", - firstProvingBlockNumber.toString() - ); - assert.fieldEquals( - "DataSet", - proofSetId, - "maxProvingPeriod", - MAX_PROVING_PERIOD.toString() - ); - assert.fieldEquals( - "DataSet", - proofSetId, - "challengeWindowSize", - CHALLENGE_WINDOW_SIZE.toString() - ); - assert.fieldEquals( - "DataSet", - proofSetId, - "nextDeadline", - expectedNextDeadline.toString() - ); - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "1"); - assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); - assert.fieldEquals("DataSet", proofSetId, "totalFaultedPeriods", "0"); - assert.fieldEquals( - "DataSet", - proofSetId, - "nextChallengeEpoch", - challengeEpoch.toString() - ); - assert.fieldEquals( - "DataSet", - proofSetId, - "challengeRange", - leafCount.toString() - ); - - assert.fieldEquals("Provider", providerId, "totalFaultedPeriods", "0"); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "1"); - - const provingWindowId = Bytes.fromUTF8(SET_ID.toString() + "-1").toHex(); - assert.entityCount("ProvingWindow", 1); - assert.fieldEquals("ProvingWindow", provingWindowId, "deadlineCount", "1"); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "deadline", - expectedNextDeadline.toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "windowStart", - expectedNextDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "windowEnd", - expectedNextDeadline.toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "proofSubmitted", - "false" - ); - assert.fieldEquals("ProvingWindow", provingWindowId, "isValid", "false"); - }); - - test("Test 3: Second nextProvingPeriod without proof submission - 1 faulted period", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const secondProvingBlockNumber = BigInt.fromI32(400); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(300), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(301), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(2000), - generateTxHash(302), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - const periodsSkipped = secondProvingBlockNumber - .minus(firstDeadline.plus(BigInt.fromI32(1))) - .div(MAX_PROVING_PERIOD); - const expectedNextDeadline = firstDeadline.plus( - MAX_PROVING_PERIOD.times(periodsSkipped.plus(BigInt.fromI32(1))) - ); - const expectedFaultedPeriods = periodsSkipped.plus(BigInt.fromI32(1)); - - assert.fieldEquals( - "DataSet", - proofSetId, - "nextDeadline", - expectedNextDeadline.toString() - ); - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); - assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); - assert.fieldEquals( - "DataSet", - proofSetId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - - assert.fieldEquals( - "Provider", - providerId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); - }); - - test("Test 4: Proof submission marks period as proven", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const proofBlockNumber = BigInt.fromI32(370); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(400), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(401), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const possessionProvenEvent = createPossessionProvenEvent( - SET_ID, - [ROOT_ID_1], - [BigInt.fromI32(100)], - CONTRACT_ADDRESS, - proofBlockNumber, - BigInt.fromI32(1800), - generateTxHash(402), - BigInt.fromI32(0) - ); - handlePossessionProven(possessionProvenEvent); - - const proofSetId = getProofSetId(); - const provingWindowId = Bytes.fromUTF8(SET_ID.toString() + "-1").toHex(); - - assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "true"); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "proofSubmitted", - "true" - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "proofBlockNumber", - proofBlockNumber.toString() - ); - assert.fieldEquals("ProvingWindow", provingWindowId, "isValid", "true"); - }); - - test("Test 5: Third nextProvingPeriod with proof - 0 faulted periods", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const proofBlockNumber = BigInt.fromI32(370); - const secondProvingBlockNumber = BigInt.fromI32(400); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(500), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(501), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const possessionProvenEvent = createPossessionProvenEvent( - SET_ID, - [ROOT_ID_1], - [BigInt.fromI32(100)], - CONTRACT_ADDRESS, - proofBlockNumber, - BigInt.fromI32(1800), - generateTxHash(502), - BigInt.fromI32(0) - ); - handlePossessionProven(possessionProvenEvent); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(2000), - generateTxHash(503), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - const periodsSkipped = secondProvingBlockNumber - .minus(firstDeadline.plus(BigInt.fromI32(1))) - .div(MAX_PROVING_PERIOD); - - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); - assert.fieldEquals("DataSet", proofSetId, "provenThisPeriod", "false"); - assert.fieldEquals( - "DataSet", - proofSetId, - "totalFaultedPeriods", - periodsSkipped.toString() - ); - - assert.fieldEquals( - "Provider", - providerId, - "totalFaultedPeriods", - periodsSkipped.toString() - ); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); - }); - - test("Test 6: Multiple periods skipped - calculates correct faulted periods", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const secondProvingBlockNumber = BigInt.fromI32(900); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(600), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(601), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(3000), - generateTxHash(602), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - const periodsSkipped = secondProvingBlockNumber - .minus(firstDeadline.plus(BigInt.fromI32(1))) - .div(MAX_PROVING_PERIOD); - const expectedFaultedPeriods = periodsSkipped.plus(BigInt.fromI32(1)); - const expectedDeadlineCount = periodsSkipped.plus(BigInt.fromI32(2)); - const expectedNextDeadline = firstDeadline.plus( - MAX_PROVING_PERIOD.times(periodsSkipped.plus(BigInt.fromI32(1))) - ); - - assert.fieldEquals( - "DataSet", - proofSetId, - "nextDeadline", - expectedNextDeadline.toString() - ); - assert.fieldEquals( - "DataSet", - proofSetId, - "currentDeadlineCount", - expectedDeadlineCount.toString() - ); - assert.fieldEquals( - "DataSet", - proofSetId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - - assert.fieldEquals( - "Provider", - providerId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - assert.fieldEquals( - "Provider", - providerId, - "totalProvingPeriods", - expectedDeadlineCount.toString() - ); - }); - - test("Test 7: nextProvingPeriod called before deadline - no periods skipped but pervious period faulted", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const secondProvingBlockNumber = BigInt.fromI32(380); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(700), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(701), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(2000), - generateTxHash(702), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - const expectedFaultedPeriods = BigInt.fromI32(1); - const expectedNextDeadline = firstDeadline.plus(MAX_PROVING_PERIOD); - - assert.fieldEquals( - "DataSet", - proofSetId, - "nextDeadline", - expectedNextDeadline.toString() - ); - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); - assert.fieldEquals( - "DataSet", - proofSetId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - - assert.fieldEquals( - "Provider", - providerId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); - }); - - test("Test 8: nextProvingPeriod called exactly at deadline - 1 faulted period", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const secondProvingBlockNumber = BigInt.fromI32(390); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(800), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(801), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(2000), - generateTxHash(802), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - const expectedFaultedPeriods = BigInt.fromI32(1); - const expectedNextDeadline = firstDeadline.plus(MAX_PROVING_PERIOD); - - assert.fieldEquals( - "DataSet", - proofSetId, - "nextDeadline", - expectedNextDeadline.toString() - ); - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "2"); - assert.fieldEquals( - "DataSet", - proofSetId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - - assert.fieldEquals( - "Provider", - providerId, - "totalFaultedPeriods", - expectedFaultedPeriods.toString() - ); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "2"); - }); - - test("Test 9: Verify ProvingWindow entities created for skipped periods", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const secondProvingBlockNumber = BigInt.fromI32(900); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(900), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(901), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const firstDeadline = firstProvingBlockNumber.plus(MAX_PROVING_PERIOD); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(3000), - generateTxHash(902), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - - const periodsSkipped = secondProvingBlockNumber - .minus(firstDeadline.plus(BigInt.fromI32(1))) - .div(MAX_PROVING_PERIOD); - const expectedDeadlineCount = periodsSkipped.plus(BigInt.fromI32(2)); - - assert.fieldEquals( - "DataSet", - proofSetId, - "currentDeadlineCount", - expectedDeadlineCount.toString() - ); - - assert.entityCount("ProvingWindow", expectedDeadlineCount.toI32()); - - const provingWindow1Id = Bytes.fromUTF8(SET_ID.toString() + "-1").toHex(); - assert.fieldEquals("ProvingWindow", provingWindow1Id, "deadlineCount", "1"); - assert.fieldEquals( - "ProvingWindow", - provingWindow1Id, - "deadline", - firstDeadline.toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindow1Id, - "windowStart", - firstDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindow1Id, - "windowEnd", - firstDeadline.toString() - ); - - for (let i = 0; i < periodsSkipped.toI32(); i++) { - const deadlineCount = expectedDeadlineCount - .minus(periodsSkipped) - .plus(BigInt.fromI32(i)); - const expectedDeadline = firstProvingBlockNumber.plus( - deadlineCount.times(MAX_PROVING_PERIOD) - ); - const provingWindowId = Bytes.fromUTF8( - SET_ID.toString() + "-" + deadlineCount.toString() - ).toHex(); - - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "deadlineCount", - deadlineCount.toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "deadline", - expectedDeadline.toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "windowStart", - expectedDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "windowEnd", - expectedDeadline.toString() - ); - assert.fieldEquals( - "ProvingWindow", - provingWindowId, - "proofSubmitted", - "false" - ); - assert.fieldEquals("ProvingWindow", provingWindowId, "isValid", "false"); - } - - const finalDeadline = firstDeadline.plus( - MAX_PROVING_PERIOD.times(periodsSkipped.plus(BigInt.fromI32(1))) - ); - const finalProvingWindowId = Bytes.fromUTF8( - SET_ID.toString() + "-" + expectedDeadlineCount.toString() - ).toHex(); - assert.fieldEquals( - "ProvingWindow", - finalProvingWindowId, - "deadlineCount", - expectedDeadlineCount.toString() - ); - assert.fieldEquals( - "ProvingWindow", - finalProvingWindowId, - "deadline", - finalDeadline.toString() - ); - assert.fieldEquals( - "ProvingWindow", - finalProvingWindowId, - "windowStart", - finalDeadline.minus(CHALLENGE_WINDOW_SIZE).toString() - ); - assert.fieldEquals( - "ProvingWindow", - finalProvingWindowId, - "windowEnd", - finalDeadline.toString() - ); - }); - - test("Test 10: Complex scenario - multiple proving periods with mixed proof submissions", () => { - const createBlockNumber = BigInt.fromI32(100); - const firstProvingBlockNumber = BigInt.fromI32(150); - const proofBlockNumber1 = BigInt.fromI32(370); - const secondProvingBlockNumber = BigInt.fromI32(400); - const thirdProvingBlockNumber = BigInt.fromI32(650); - const proofBlockNumber2 = BigInt.fromI32(870); - const fourthProvingBlockNumber = BigInt.fromI32(900); - const challengeEpoch = BigInt.fromI32(200); - const leafCount = BigInt.fromI32(1000); - - const dataSetCreatedEvent = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - CONTRACT_ADDRESS, - LISTENER_ADDRESS, - createBlockNumber, - BigInt.fromI32(1000), - generateTxHash(1000), - BigInt.fromI32(0) - ); - handleDataSetCreated(dataSetCreatedEvent); - addRootToDataSet(SET_ID, ROOT_ID_1); - - const firstNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch, - leafCount, - CONTRACT_ADDRESS, - firstProvingBlockNumber, - BigInt.fromI32(1500), - generateTxHash(1001), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(firstNextProvingPeriodEvent); - - const possessionProvenEvent1 = createPossessionProvenEvent( - SET_ID, - [ROOT_ID_1], - [BigInt.fromI32(100)], - CONTRACT_ADDRESS, - proofBlockNumber1, - BigInt.fromI32(1800) - ); - handlePossessionProven(possessionProvenEvent1); - - const secondNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(100)), - leafCount, - CONTRACT_ADDRESS, - secondProvingBlockNumber, - BigInt.fromI32(2000), - generateTxHash(1002), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(secondNextProvingPeriodEvent); - - const thirdNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(200)), - leafCount, - CONTRACT_ADDRESS, - thirdProvingBlockNumber, - BigInt.fromI32(2500), - generateTxHash(1003), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(thirdNextProvingPeriodEvent); - - const possessionProvenEvent2 = createPossessionProvenEvent( - SET_ID, - [ROOT_ID_1], - [BigInt.fromI32(100)], - CONTRACT_ADDRESS, - proofBlockNumber2, - BigInt.fromI32(3000) - ); - handlePossessionProven(possessionProvenEvent2); - - const fourthNextProvingPeriodEvent = createNextProvingPeriodEvent( - SET_ID, - challengeEpoch.plus(BigInt.fromI32(300)), - leafCount, - CONTRACT_ADDRESS, - fourthProvingBlockNumber, - BigInt.fromI32(3500), - generateTxHash(1004), - BigInt.fromI32(0) - ); - handleNextProvingPeriod(fourthNextProvingPeriodEvent); - - const proofSetId = getProofSetId(); - const providerId = getProviderId(); - - const expectedTotalFaultedPeriods = BigInt.fromI32(1); - - assert.fieldEquals("DataSet", proofSetId, "currentDeadlineCount", "4"); - assert.fieldEquals( - "DataSet", - proofSetId, - "totalFaultedPeriods", - expectedTotalFaultedPeriods.toString() - ); - - assert.fieldEquals( - "Provider", - providerId, - "totalFaultedPeriods", - expectedTotalFaultedPeriods.toString() - ); - assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "4"); - }); -}); diff --git a/apps/subgraph/tests/fwss.test.ts b/apps/subgraph/tests/fwss.test.ts index 6a8433ad..ce359d1b 100644 --- a/apps/subgraph/tests/fwss.test.ts +++ b/apps/subgraph/tests/fwss.test.ts @@ -1,71 +1,40 @@ -import { - assert, - describe, - test, - clearStore, - beforeEach, -} from "matchstick-as/assembly/index"; -import { BigInt, Address, Bytes } from "@graphprotocol/graph-ts"; -import { - handleDataSetCreated, - handlePiecesAdded, - getRootEntityId, -} from "../src/pdp-verifier"; +import { assert, beforeEach, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { getRootEntityId, handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; import { handleFwssDataSetCreated, + handleFwssDataSetServiceProviderChanged, + handleFwssPdpPaymentTerminated, handleFwssPieceAdded, handleFwssServiceTerminated, - handleFwssPdpPaymentTerminated, - handleFwssDataSetServiceProviderChanged, } from "../src/fwss"; -import { - createDataSetCreatedEvent, - createRootsAddedEvent, -} from "./pdp-verifier-utils"; +import { createDataSetCreatedEvent, createRootsAddedEvent } from "./pdp-verifier-utils"; import { createFwssDataSetCreatedEvent, + createFwssDataSetServiceProviderChangedEvent, + createFwssPdpPaymentTerminatedEvent, createFwssPieceAddedEvent, createFwssServiceTerminatedEvent, - createFwssPdpPaymentTerminatedEvent, - createFwssDataSetServiceProviderChangedEvent, } from "./fwss-utils"; const SET_ID = BigInt.fromI32(1); const PROVIDER_ID = BigInt.fromI32(42); const PDP_RAIL_ID = BigInt.fromI32(99); const ROOT_ID = BigInt.fromI32(101); -const PROVIDER_ADDRESS = Address.fromString( - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" -); -const PAYER_ADDRESS = Address.fromString( - "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" -); -const NEW_PROVIDER_ADDRESS = Address.fromString( - "0xc16081f360e3847006db660bae1c6d1b2e17ec2c" -); -const CONTRACT_ADDRESS = Address.fromString( - "0xd16081f360e3847006db660bae1c6d1b2e17ec2d" -); +const PROVIDER_ADDRESS = Address.fromString("0xa16081f360e3847006db660bae1c6d1b2e17ec2a"); +const PAYER_ADDRESS = Address.fromString("0xb16081f360e3847006db660bae1c6d1b2e17ec2b"); +const NEW_PROVIDER_ADDRESS = Address.fromString("0xc16081f360e3847006db660bae1c6d1b2e17ec2c"); +const CONTRACT_ADDRESS = Address.fromString("0xd16081f360e3847006db660bae1c6d1b2e17ec2d"); const PROOF_SET_ENTITY_ID = Bytes.fromByteArray(Bytes.fromBigInt(SET_ID)); function seedDataSet(): void { - let ev = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - Bytes.fromI32(0), - CONTRACT_ADDRESS - ); + const ev = createDataSetCreatedEvent(SET_ID, PROVIDER_ADDRESS, Bytes.fromI32(0), CONTRACT_ADDRESS); handleDataSetCreated(ev); } function seedRoot(): void { - let ev = createRootsAddedEvent( - SET_ID, - [ROOT_ID], - PROVIDER_ADDRESS, - CONTRACT_ADDRESS - ); + const ev = createRootsAddedEvent(SET_ID, [ROOT_ID], PROVIDER_ADDRESS, CONTRACT_ADDRESS); handlePiecesAdded(ev); } @@ -76,110 +45,48 @@ describe("FWSS handlers", () => { // -- handleFwssDataSetCreated ------------------------------------------- - test("PDPVerifier-created DataSet has default FWSS fields", () => { + test("PDPVerifier-created DataSet has withIPFSIndexing = false by default", () => { seedDataSet(); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withIPFSIndexing", - "false" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withCDN", - "false" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "metadataKeys", - "[]" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "metadataValues", - "[]" - ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "withIPFSIndexing", "false"); }); test("handleFwssDataSetCreated populates FWSS fields and derives withIPFSIndexing", () => { seedDataSet(); - let ev = createFwssDataSetCreatedEvent( + const ev = createFwssDataSetCreatedEvent( SET_ID, PROVIDER_ID, PDP_RAIL_ID, PAYER_ADDRESS, PROVIDER_ADDRESS, ["source", "withIPFSIndexing", "withCDN"], - ["filecoin-pin", "", "true"] + ["filecoin-pin", "", "true"], ); handleFwssDataSetCreated(ev); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "fwssProviderId", - PROVIDER_ID.toString() - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "fwssPayer", - PAYER_ADDRESS.toHexString() - ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "fwssPayer", PAYER_ADDRESS.toHexString()); assert.fieldEquals( "DataSet", PROOF_SET_ENTITY_ID.toHexString(), "fwssServiceProvider", - PROVIDER_ADDRESS.toHexString() - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "fwssPdpRailId", - PDP_RAIL_ID.toString() - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withIPFSIndexing", - "true" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withCDN", - "true" + PROVIDER_ADDRESS.toHexString(), ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "withIPFSIndexing", "true"); }); - test("handleFwssDataSetCreated leaves booleans false when keys absent", () => { + test("handleFwssDataSetCreated leaves withIPFSIndexing false when key absent", () => { seedDataSet(); - let ev = createFwssDataSetCreatedEvent( + const ev = createFwssDataSetCreatedEvent( SET_ID, PROVIDER_ID, PDP_RAIL_ID, PAYER_ADDRESS, PROVIDER_ADDRESS, ["source"], - ["filecoin-pin"] + ["filecoin-pin"], ); handleFwssDataSetCreated(ev); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withIPFSIndexing", - "false" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withCDN", - "false" - ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "withIPFSIndexing", "false"); }); test("handleFwssDataSetCreated creates a stub when DataSet doesn't exist yet", () => { @@ -188,27 +95,23 @@ describe("FWSS handlers", () => { // must create a stub with FWSS fields set so the later PDPVerifier handler // can load it instead of overwriting. const UNSEEN_SET_ID = BigInt.fromI32(999); - const unseenEntityId = Bytes.fromByteArray( - Bytes.fromBigInt(UNSEEN_SET_ID) - ).toHexString(); + const unseenEntityId = Bytes.fromByteArray(Bytes.fromBigInt(UNSEEN_SET_ID)).toHexString(); - let ev = createFwssDataSetCreatedEvent( + const ev = createFwssDataSetCreatedEvent( UNSEEN_SET_ID, PROVIDER_ID, PDP_RAIL_ID, PAYER_ADDRESS, PROVIDER_ADDRESS, ["withIPFSIndexing"], - [""] + [""], ); handleFwssDataSetCreated(ev); - // Stub was created with FWSS fields populated. assert.fieldEquals("DataSet", unseenEntityId, "setId", "999"); assert.fieldEquals("DataSet", unseenEntityId, "fwssPayer", PAYER_ADDRESS.toHexString()); assert.fieldEquals("DataSet", unseenEntityId, "withIPFSIndexing", "true"); - // Placeholder owner/listener set by the FWSS handler (pdp-verifier will - // overwrite when it runs later in the same block). + // Placeholder owner set by the FWSS stub (pdp-verifier overwrites later in the same block). assert.fieldEquals("DataSet", unseenEntityId, "owner", PROVIDER_ADDRESS.toHexString()); }); @@ -216,85 +119,40 @@ describe("FWSS handlers", () => { // Simulates real on-chain ordering: FWSS.DataSetCreated fires before // PDPVerifier.DataSetCreated. After both handlers run, FWSS and // PDPVerifier fields must both be populated correctly. - let fwssEv = createFwssDataSetCreatedEvent( + const fwssEv = createFwssDataSetCreatedEvent( SET_ID, PROVIDER_ID, PDP_RAIL_ID, PAYER_ADDRESS, PROVIDER_ADDRESS, - ["withIPFSIndexing", "withCDN"], - ["", "true"] + ["withIPFSIndexing"], + [""], ); handleFwssDataSetCreated(fwssEv); - // Then PDPVerifier fires, which must load the stub (not overwrite it). - let pdpEv = createDataSetCreatedEvent( - SET_ID, - PROVIDER_ADDRESS, - Bytes.fromI32(0), - CONTRACT_ADDRESS - ); + const pdpEv = createDataSetCreatedEvent(SET_ID, PROVIDER_ADDRESS, Bytes.fromI32(0), CONTRACT_ADDRESS); handleDataSetCreated(pdpEv); - // FWSS fields preserved - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "fwssProviderId", - PROVIDER_ID.toString() - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withIPFSIndexing", - "true" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "withCDN", - "true" - ); - // PDPVerifier fields set - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "setId", - SET_ID.toString() - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "isActive", - "true" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "status", - "EMPTY" - ); + // FWSS fields preserved. + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "withIPFSIndexing", "true"); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "fwssPayer", PAYER_ADDRESS.toHexString()); + // PDPVerifier fields set. + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "setId", SET_ID.toString()); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "isActive", "true"); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "status", "EMPTY"); }); // -- handleFwssPieceAdded ----------------------------------------------- - test("PDPVerifier-created Root has default FWSS fields", () => { - seedDataSet(); - seedRoot(); - const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); - assert.fieldEquals("Root", rootId, "metadataKeys", "[]"); - assert.fieldEquals("Root", rootId, "metadataValues", "[]"); - }); - test("handleFwssPieceAdded extracts ipfsRootCID", () => { seedDataSet(); seedRoot(); - let ev = createFwssPieceAddedEvent( + const ev = createFwssPieceAddedEvent( SET_ID, ROOT_ID, Bytes.fromHexString("0xdeadbeef"), ["ipfsRootCID"], - ["bafybeiexamplecid"] + ["bafybeiexamplecid"], ); handleFwssPieceAdded(ev); @@ -302,33 +160,15 @@ describe("FWSS handlers", () => { assert.fieldEquals("Root", rootId, "ipfsRootCID", "bafybeiexamplecid"); }); - test("handleFwssPieceAdded leaves ipfsRootCID null when absent", () => { - seedDataSet(); - seedRoot(); - let ev = createFwssPieceAddedEvent( - SET_ID, - ROOT_ID, - Bytes.fromHexString("0xdeadbeef"), - [], - [] - ); - handleFwssPieceAdded(ev); - - // When a nullable field has no value, matchstick's fieldEquals with "null" - // matches. Verify no crash and empty arrays persist. - const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); - assert.fieldEquals("Root", rootId, "metadataKeys", "[]"); - }); - test("handleFwssPieceAdded no-ops for unknown pieceId", () => { seedDataSet(); - // no seedRoot — root doesn't exist - let ev = createFwssPieceAddedEvent( + // no seedRoot — root doesn't exist. + const ev = createFwssPieceAddedEvent( SET_ID, BigInt.fromI32(999), Bytes.fromHexString("0xdeadbeef"), ["ipfsRootCID"], - ["bafybeinope"] + ["bafybeinope"], ); handleFwssPieceAdded(ev); @@ -340,116 +180,102 @@ describe("FWSS handlers", () => { test("handleFwssServiceTerminated flips isActive to false", () => { seedDataSet(); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "isActive", - "true" - ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "isActive", "true"); - let ev = createFwssServiceTerminatedEvent(SET_ID, PROVIDER_ADDRESS); + const ev = createFwssServiceTerminatedEvent(SET_ID, PROVIDER_ADDRESS); handleFwssServiceTerminated(ev); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "isActive", - "false" - ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "isActive", "false"); }); test("handleFwssServiceTerminated no-ops for unknown dataSetId", () => { - let ev = createFwssServiceTerminatedEvent( - BigInt.fromI32(999), - PROVIDER_ADDRESS - ); + const ev = createFwssServiceTerminatedEvent(BigInt.fromI32(999), PROVIDER_ADDRESS); handleFwssServiceTerminated(ev); - assert.notInStore( - "DataSet", - Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() - ); + assert.notInStore("DataSet", Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString()); }); // -- handleFwssPdpPaymentTerminated ------------------------------------- test("handleFwssPdpPaymentTerminated stores endEpoch and leaves isActive alone", () => { seedDataSet(); - let ev = createFwssPdpPaymentTerminatedEvent( - SET_ID, - BigInt.fromI32(12345), - PDP_RAIL_ID - ); + const ev = createFwssPdpPaymentTerminatedEvent(SET_ID, BigInt.fromI32(12345), PDP_RAIL_ID); handleFwssPdpPaymentTerminated(ev); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "pdpPaymentEndEpoch", - "12345" - ); - assert.fieldEquals( - "DataSet", - PROOF_SET_ENTITY_ID.toHexString(), - "isActive", - "true" - ); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "pdpPaymentEndEpoch", "12345"); + assert.fieldEquals("DataSet", PROOF_SET_ENTITY_ID.toHexString(), "isActive", "true"); }); test("handleFwssPdpPaymentTerminated no-ops for unknown dataSetId", () => { - let ev = createFwssPdpPaymentTerminatedEvent( - BigInt.fromI32(999), - BigInt.fromI32(12345), - PDP_RAIL_ID - ); + const ev = createFwssPdpPaymentTerminatedEvent(BigInt.fromI32(999), BigInt.fromI32(12345), PDP_RAIL_ID); handleFwssPdpPaymentTerminated(ev); - assert.notInStore( - "DataSet", - Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() - ); + assert.notInStore("DataSet", Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString()); }); // -- handleFwssDataSetServiceProviderChanged ---------------------------- test("handleFwssDataSetServiceProviderChanged updates fwssServiceProvider", () => { seedDataSet(); - // seed with FWSS initial state handleFwssDataSetCreated( - createFwssDataSetCreatedEvent( - SET_ID, - PROVIDER_ID, - PDP_RAIL_ID, - PAYER_ADDRESS, - PROVIDER_ADDRESS, - [], - [] - ) + createFwssDataSetCreatedEvent(SET_ID, PROVIDER_ID, PDP_RAIL_ID, PAYER_ADDRESS, PROVIDER_ADDRESS, [], []), ); - let ev = createFwssDataSetServiceProviderChangedEvent( - SET_ID, - PROVIDER_ADDRESS, - NEW_PROVIDER_ADDRESS - ); + const ev = createFwssDataSetServiceProviderChangedEvent(SET_ID, PROVIDER_ADDRESS, NEW_PROVIDER_ADDRESS); handleFwssDataSetServiceProviderChanged(ev); assert.fieldEquals( "DataSet", PROOF_SET_ENTITY_ID.toHexString(), "fwssServiceProvider", - NEW_PROVIDER_ADDRESS.toHexString() + NEW_PROVIDER_ADDRESS.toHexString(), ); }); test("handleFwssDataSetServiceProviderChanged no-ops for unknown dataSetId", () => { - let ev = createFwssDataSetServiceProviderChangedEvent( + const ev = createFwssDataSetServiceProviderChangedEvent( BigInt.fromI32(999), PROVIDER_ADDRESS, - NEW_PROVIDER_ADDRESS + NEW_PROVIDER_ADDRESS, ); handleFwssDataSetServiceProviderChanged(ev); - assert.notInStore( - "DataSet", - Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString() + assert.notInStore("DataSet", Bytes.fromByteArray(Bytes.fromBigInt(BigInt.fromI32(999))).toHexString()); + }); + + // -- End-to-end: backs GET_FWSS_CANDIDATE_PIECES query ------------------ + + test("GET_FWSS_CANDIDATE_PIECES: DataSet + Root populated with filterable fields", () => { + // FWSS stub first (matches on-chain ordering). + const fwssDsEv = createFwssDataSetCreatedEvent( + SET_ID, + PROVIDER_ID, + PDP_RAIL_ID, + PAYER_ADDRESS, + PROVIDER_ADDRESS, + ["withIPFSIndexing"], + [""], + ); + handleFwssDataSetCreated(fwssDsEv); + + // PDPVerifier DataSetCreated fills in PDPVerifier-layer fields. + handleDataSetCreated(createDataSetCreatedEvent(SET_ID, PROVIDER_ADDRESS, Bytes.fromI32(0), CONTRACT_ADDRESS)); + + // PiecesAdded creates Root. + handlePiecesAdded(createRootsAddedEvent(SET_ID, [ROOT_ID], PROVIDER_ADDRESS, CONTRACT_ADDRESS)); + + // FWSS.PieceAdded adds ipfsRootCID. + handleFwssPieceAdded( + createFwssPieceAddedEvent(SET_ID, ROOT_ID, Bytes.fromHexString("0xdeadbeef"), ["ipfsRootCID"], [ + "bafybeiexamplecid", + ]), ); + + const dsId = PROOF_SET_ENTITY_ID.toHexString(); + assert.fieldEquals("DataSet", dsId, "isActive", "true"); + assert.fieldEquals("DataSet", dsId, "withIPFSIndexing", "true"); + assert.fieldEquals("DataSet", dsId, "fwssPayer", PAYER_ADDRESS.toHexString()); + assert.fieldEquals("DataSet", dsId, "fwssServiceProvider", PROVIDER_ADDRESS.toHexString()); + + const rootId = getRootEntityId(SET_ID, ROOT_ID).toHexString(); + assert.fieldEquals("Root", rootId, "removed", "false"); + assert.fieldEquals("Root", rootId, "ipfsRootCID", "bafybeiexamplecid"); }); }); diff --git a/apps/subgraph/tests/pdp-verifier.test.ts b/apps/subgraph/tests/pdp-verifier.test.ts index 9f117cfa..805c306b 100644 --- a/apps/subgraph/tests/pdp-verifier.test.ts +++ b/apps/subgraph/tests/pdp-verifier.test.ts @@ -1,81 +1,34 @@ -import { - assert, - describe, - test, - clearStore, - beforeAll, - afterAll, -} from "matchstick-as/assembly/index"; -import { BigInt, Address, Bytes, ByteArray } from "@graphprotocol/graph-ts"; -import { - handlePiecesAdded, - handleDataSetCreated, - getRootEntityId, -} from "../src/pdp-verifier"; -import { - createRootsAddedEvent, - createDataSetCreatedEvent, -} from "./pdp-verifier-utils"; +import { afterAll, assert, beforeAll, clearStore, describe, test } from "matchstick-as/assembly/index"; +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { getRootEntityId, handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; +import { createDataSetCreatedEvent, createRootsAddedEvent } from "./pdp-verifier-utils"; -// Define constants for test data const SET_ID = BigInt.fromI32(1); const ROOT_ID_1 = BigInt.fromI32(101); const RAW_SIZE_1 = BigInt.fromI32(10486897); -// CIDs as strings const ROOT_CID_1_STR = "0x01559120258ff7f7021387dcea7164b7d1c4a98bd6f8d3c187e3114795efa391df307c8aa9d5d5cbac03"; -const SENDER_ADDRESS = Address.fromString( - "0xa16081f360e3847006db660bae1c6d1b2e17ec2a" -); -const LISTENER_ADDRESS = Address.fromString( - "0x0000000000000000000000000000000000000001" -); -const CONTRACT_ADDRESS = Address.fromString( - "0xb16081f360e3847006db660bae1c6d1b2e17ec2b" -); +const SENDER_ADDRESS = Address.fromString("0xa16081f360e3847006db660bae1c6d1b2e17ec2a"); +const CONTRACT_ADDRESS = Address.fromString("0xb16081f360e3847006db660bae1c6d1b2e17ec2b"); const PROOF_SET_ID_BYTES = Bytes.fromBigInt(SET_ID); -// Helper to convert string to Bytes and pad to 32 bytes -function stringToBytes32(str: string): Bytes { - let utf8Bytes = Bytes.fromUTF8(str); - let paddedBytes = new ByteArray(32); // Create a 32-byte array, initialized to zeros - - // Copy bytes from utf8Bytes, ensuring we don't exceed 32 bytes - for (let i = 0; i < utf8Bytes.length && i < 32; i++) { - paddedBytes[i] = utf8Bytes[i]; - } - return Bytes.fromByteArray(paddedBytes); -} - describe("handlePiecesAdded Tests", () => { beforeAll(() => { - // 1. Create the necessary DataSet first - let mockDataSetCreatedEvent = createDataSetCreatedEvent( + const mockDataSetCreatedEvent = createDataSetCreatedEvent( SET_ID, SENDER_ADDRESS, - Bytes.fromI32(123), // Dummy root, as it's required by the function but not used by the handler here + Bytes.fromI32(123), CONTRACT_ADDRESS, - BigInt.fromI32(50), // Match block number for consistency - BigInt.fromI32(1678886400) // Match timestamp for consistency + BigInt.fromI32(50), + BigInt.fromI32(1678886400), ); handleDataSetCreated(mockDataSetCreatedEvent); - // 2. Create and handle the piecesAdded event - let pieceIds = [ROOT_ID_1]; - let rootsAddedEvent = createRootsAddedEvent( - SET_ID, - pieceIds, - SENDER_ADDRESS, - CONTRACT_ADDRESS - ); - - // Set block/tx details on the mock event if needed by handler - rootsAddedEvent.block.timestamp = BigInt.fromI32(100); // Example timestamp - rootsAddedEvent.block.number = BigInt.fromI32(50); // Example block number - rootsAddedEvent.logIndex = BigInt.fromI32(1); // Example log index - rootsAddedEvent.transaction.hash = Bytes.fromHexString( - "0x" + "c".repeat(64) - ); + const rootsAddedEvent = createRootsAddedEvent(SET_ID, [ROOT_ID_1], SENDER_ADDRESS, CONTRACT_ADDRESS); + rootsAddedEvent.block.timestamp = BigInt.fromI32(100); + rootsAddedEvent.block.number = BigInt.fromI32(50); + rootsAddedEvent.logIndex = BigInt.fromI32(1); + rootsAddedEvent.transaction.hash = Bytes.fromHexString("0x" + "c".repeat(64)); handlePiecesAdded(rootsAddedEvent); }); @@ -84,67 +37,27 @@ describe("handlePiecesAdded Tests", () => { clearStore(); }); - test("Entities created and stored correctly", () => { - // Assert counts + test("DataSet, Provider, and Root are created with the expected fields", () => { assert.entityCount("DataSet", 1); - assert.entityCount("Root", 1); // One root was added + assert.entityCount("Root", 1); assert.entityCount("Provider", 1); - assert.entityCount("EventLog", 2); // piecesAdded creates one event log - // --- Assert DataSet fields --- - let dataSetId = PROOF_SET_ID_BYTES.toHex(); + const dataSetId = PROOF_SET_ID_BYTES.toHex(); assert.fieldEquals("DataSet", dataSetId, "setId", SET_ID.toString()); - assert.fieldEquals("DataSet", dataSetId, "totalRoots", "1"); // Initially 0, added 1 - let expectedTotalSize = RAW_SIZE_1.toString(); - assert.fieldEquals( - "DataSet", - dataSetId, - "totalDataSize", - expectedTotalSize - ); - assert.fieldEquals("DataSet", dataSetId, "updatedAt", "100"); - assert.fieldEquals("DataSet", dataSetId, "blockNumber", "50"); - - // --- Assert Root fields --- - let rootEntityId1 = getRootEntityId(SET_ID, ROOT_ID_1).toHex(); - assert.fieldEquals("Root", rootEntityId1, "rootId", ROOT_ID_1.toString()); - assert.fieldEquals("Root", rootEntityId1, "setId", SET_ID.toString()); - assert.fieldEquals("Root", rootEntityId1, "cid", ROOT_CID_1_STR); - assert.fieldEquals("Root", rootEntityId1, "rawSize", RAW_SIZE_1.toString()); - // assert.fieldEquals("Root", rootEntityId1, "createdAt", "100"); - assert.fieldEquals("Root", rootEntityId1, "blockNumber", "50"); - - // --- Assert Provider fields --- - let providerId = SENDER_ADDRESS.toHex(); - assert.fieldEquals( - "Provider", - providerId, - "totalDataSize", - expectedTotalSize - ); - assert.fieldEquals("Provider", providerId, "updatedAt", "100"); - assert.fieldEquals("Provider", providerId, "blockNumber", "50"); - // Assuming provider was newly created by this event - // assert.fieldEquals("Provider", providerId, "createdAt", "100"); - - // --- Assert EventLog fields --- - // Construct expected event ID: txHash + logIndex - let eventId = Bytes.fromHexString("0x" + "c".repeat(64)) - .concatI32(BigInt.fromI32(1).toI32()) - .toHex(); - assert.fieldEquals("EventLog", eventId, "name", "piecesAdded"); - assert.fieldEquals("EventLog", eventId, "setId", SET_ID.toString()); - assert.fieldEquals( - "EventLog", - eventId, - "transactionHash", - "0x" + "c".repeat(64) - ); - assert.fieldEquals("EventLog", eventId, "blockNumber", "50"); - assert.fieldEquals("EventLog", eventId, "logIndex", "1"); - assert.fieldEquals("EventLog", eventId, "createdAt", "100"); - // Check data field (simple representation) - let expectedData = `{ "setId": "${SET_ID.toString()}", "pieceIds": [${ROOT_ID_1.toString()}] }`; - assert.fieldEquals("EventLog", eventId, "data", expectedData); + assert.fieldEquals("DataSet", dataSetId, "status", "READY"); + assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); + assert.fieldEquals("DataSet", dataSetId, "owner", SENDER_ADDRESS.toHex()); + + const rootEntityId = getRootEntityId(SET_ID, ROOT_ID_1).toHex(); + assert.fieldEquals("Root", rootEntityId, "rootId", ROOT_ID_1.toString()); + assert.fieldEquals("Root", rootEntityId, "setId", SET_ID.toString()); + assert.fieldEquals("Root", rootEntityId, "cid", ROOT_CID_1_STR); + assert.fieldEquals("Root", rootEntityId, "rawSize", RAW_SIZE_1.toString()); + assert.fieldEquals("Root", rootEntityId, "removed", "false"); + + const providerId = SENDER_ADDRESS.toHex(); + assert.fieldEquals("Provider", providerId, "address", providerId); + assert.fieldEquals("Provider", providerId, "totalFaultedPeriods", "0"); + assert.fieldEquals("Provider", providerId, "totalProvingPeriods", "0"); }); }); diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 9aff8c1e..212b97a8 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -430,16 +430,19 @@ Session keys are scoped (only storage operations, not deposits or withdrawals) a - **Required**: No - **Default**: Empty string (feature disabled) -**Role**: The Graph API endpoint for querying PDP (Proof of Data Possession) subgraph data. This endpoint is used to retrieve data retention info for provider data. +**Role**: The Graph API endpoint for querying PDP (Proof of Data Possession) subgraph data. Drives the overdue-periods metric and the anonymous-retrieval candidate-piece query. + +The dealbot-owned subgraph lives at `apps/subgraph/` (package `@dealbot/subgraph`) and is deployed to Goldsky. Point this variable at one of those slots; the exact slugs are documented in `apps/subgraph/README.md`. **When to update**: -- When switching between different Graph API endpoints +- When swapping between the dealbot-owned subgraph slots on Goldsky (mainnet vs calibnet). +- When deploying a new subgraph version. **Example**: ```bash -PDP_SUBGRAPH_ENDPOINT=https://api.thegraph.com/subgraphs/filecoin/pdp +PDP_SUBGRAPH_ENDPOINT=https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn ``` --- From 256e0a4a9399b21158319189f42714a288217cfa Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 10:29:45 +0200 Subject: [PATCH 06/19] refactor(subgraph): rename calibnet to calibration --- apps/subgraph/README.md | 14 +++++++------- apps/subgraph/package.json | 6 +++--- apps/subgraph/subgraph.yaml | 13 +++++-------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/apps/subgraph/README.md b/apps/subgraph/README.md index 26332b00..ff0532f6 100644 --- a/apps/subgraph/README.md +++ b/apps/subgraph/README.md @@ -37,9 +37,9 @@ The package is intentionally isolated from the root `pnpm test` / `pnpm build` s | calibration (`filecoin-testnet`) | PDPVerifier | `0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C` | 3140755 | | calibration (`filecoin-testnet`) | FilecoinWarmStorageService | `0x02925630df557F957f70E112bA06e50965417CA0` | 3141276 | -Maintained in `networks.json`. Editing `subgraph.yaml` manually is usually wrong — run `pnpm build:mainnet` or `pnpm build:calibnet` which applies `networks.json` via `graph build --network `. +Maintained in `networks.json`. Editing `subgraph.yaml` manually is usually wrong — run `pnpm build:mainnet` or `pnpm build:calibration` which applies `networks.json` via `graph build --network `. -Note: `graph build --network X` rewrites `subgraph.yaml` **in place** with the chosen network's values. The committed version is mainnet-default — after a `build:calibnet`, re-run `build:mainnet` before committing to avoid leaking calibnet values into the mainnet manifest. +Note: `graph build --network X` rewrites `subgraph.yaml` **in place** with the chosen network's values. The committed version is mainnet-default — after a `build:calibration`, re-run `build:mainnet` before committing to avoid leaking calibration values into the mainnet manifest. ## Local commands @@ -49,7 +49,7 @@ pnpm --filter @dealbot/subgraph codegen # Full build for one network pnpm --filter @dealbot/subgraph build:mainnet -pnpm --filter @dealbot/subgraph build:calibnet +pnpm --filter @dealbot/subgraph build:calibration # Run matchstick tests pnpm --filter @dealbot/subgraph test @@ -61,8 +61,8 @@ Requires `goldsky` CLI authenticated via `GOLDSKY_API_KEY`. ```bash export VERSION=0.1.0 -pnpm --filter @dealbot/subgraph build:calibnet -pnpm --filter @dealbot/subgraph deploy:calibnet +pnpm --filter @dealbot/subgraph build:calibration +pnpm --filter @dealbot/subgraph deploy:calibration pnpm --filter @dealbot/subgraph build:mainnet pnpm --filter @dealbot/subgraph deploy:mainnet @@ -70,7 +70,7 @@ pnpm --filter @dealbot/subgraph deploy:mainnet Goldsky slots (slugs TBD): -- `dealbot-subgraph/` — mainnet -- `dealbot-subgraph-calibnet/` — calibration +- `dealbot-mainnet/` — mainnet +- `dealbot-calibration/` — calibration After deploy, update `PDP_SUBGRAPH_ENDPOINT` in the backend env to the new `/gn` URL. diff --git a/apps/subgraph/package.json b/apps/subgraph/package.json index b71ad3ff..3a9a87e5 100644 --- a/apps/subgraph/package.json +++ b/apps/subgraph/package.json @@ -5,9 +5,9 @@ "scripts": { "codegen": "graph codegen", "build:mainnet": "graph codegen && graph build --network filecoin", - "build:calibnet": "graph codegen && graph build --network filecoin-testnet", - "deploy:mainnet": "goldsky subgraph deploy dealbot-subgraph/$VERSION --path ./build", - "deploy:calibnet": "goldsky subgraph deploy dealbot-subgraph-calibnet/$VERSION --path ./build", + "build:calibration": "graph codegen && graph build --network filecoin-testnet", + "deploy:mainnet": "goldsky subgraph deploy dealbot-mainnet/$VERSION --path ./build", + "deploy:calibration": "goldsky subgraph deploy dealbot-calibration/$VERSION --path ./build", "test": "graph test" }, "dependencies": { diff --git a/apps/subgraph/subgraph.yaml b/apps/subgraph/subgraph.yaml index a2ce05f2..aa4faf76 100644 --- a/apps/subgraph/subgraph.yaml +++ b/apps/subgraph/subgraph.yaml @@ -15,6 +15,7 @@ dataSources: kind: ethereum/events apiVersion: 0.0.9 language: wasm/assemblyscript + file: ./src/pdp-verifier.ts entities: - DataSet - Provider @@ -39,7 +40,6 @@ dataSources: handler: handlePossessionProven - event: NextProvingPeriod(indexed uint256,uint256,uint256) handler: handleNextProvingPeriod - file: ./src/pdp-verifier.ts - kind: ethereum name: FilecoinWarmStorageService network: filecoin @@ -51,6 +51,7 @@ dataSources: kind: ethereum/events apiVersion: 0.0.9 language: wasm/assemblyscript + file: ./src/fwss.ts entities: - DataSet - Root @@ -58,17 +59,13 @@ dataSources: - name: FilecoinWarmStorageService file: ./abis/FilecoinWarmStorageService.json eventHandlers: - - event: DataSetCreated(indexed uint256,indexed - uint256,uint256,uint256,uint256,address,address,address,string[],string[]) + - event: DataSetCreated(indexed uint256,indexed uint256,uint256,uint256,uint256,address,address,address,string[],string[]) handler: handleFwssDataSetCreated - event: PieceAdded(indexed uint256,indexed uint256,(bytes),string[],string[]) handler: handleFwssPieceAdded - - event: ServiceTerminated(indexed address,indexed - uint256,uint256,uint256,uint256) + - event: ServiceTerminated(indexed address,indexed uint256,uint256,uint256,uint256) handler: handleFwssServiceTerminated - event: PDPPaymentTerminated(indexed uint256,uint256,uint256) handler: handleFwssPdpPaymentTerminated - - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed - address) + - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed address) handler: handleFwssDataSetServiceProviderChanged - file: ./src/fwss.ts From 300b5c9dc7a43defc4f2259138f83346fd9a9afe Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 11:02:12 +0200 Subject: [PATCH 07/19] refactor(subgraph): consolidate helper methods --- apps/subgraph/schema.graphql | 9 ++++ apps/subgraph/src/fwss.ts | 27 ++-------- .../subgraph/{utils/cid.ts => src/helpers.ts} | 52 ++++++++++++++++++- apps/subgraph/src/pdp-verifier.ts | 25 ++++----- apps/subgraph/subgraph.yaml | 9 ++-- apps/subgraph/tests/fwss.test.ts | 3 +- apps/subgraph/tests/pdp-verifier.test.ts | 3 +- apps/subgraph/utils/index.ts | 6 --- 8 files changed, 84 insertions(+), 50 deletions(-) rename apps/subgraph/{utils/cid.ts => src/helpers.ts} (58%) delete mode 100644 apps/subgraph/utils/index.ts diff --git a/apps/subgraph/schema.graphql b/apps/subgraph/schema.graphql index 1dadcf57..63a5fd79 100644 --- a/apps/subgraph/schema.graphql +++ b/apps/subgraph/schema.graphql @@ -48,6 +48,11 @@ type DataSet @entity(immutable: false) { # Does NOT flip isActive — clients compare to current epoch. pdpPaymentEndEpoch: BigInt + # Block timestamp at which this DataSet was first created. Set once on + # the creating event (whichever of FWSS.DataSetCreated or + # PDPVerifier.DataSetCreated fires first) and never updated. + createdAt: BigInt! + # Derived relationship roots: [Root!]! @derivedFrom(field: "proofSet") } @@ -63,5 +68,9 @@ type Root @entity(immutable: false) { # Populated by FWSS.PieceAdded handler (null for non-FWSS pieces). ipfsRootCID: String + # Block timestamp at which this Root was first added via PiecesAdded. + # Set once on creation and never updated. + createdAt: BigInt! + proofSet: DataSet! } diff --git a/apps/subgraph/src/fwss.ts b/apps/subgraph/src/fwss.ts index 1012a0e1..2e739b36 100644 --- a/apps/subgraph/src/fwss.ts +++ b/apps/subgraph/src/fwss.ts @@ -1,4 +1,4 @@ -import { BigInt, Bytes, log } from "@graphprotocol/graph-ts"; +import { BigInt, log } from "@graphprotocol/graph-ts"; import { DataSetCreated as DataSetCreatedEvent, DataSetServiceProviderChanged as DataSetServiceProviderChangedEvent, @@ -7,31 +7,9 @@ import { ServiceTerminated as ServiceTerminatedEvent, } from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService"; import { DataSet, Root } from "../generated/schema"; -import { getRootEntityId } from "./pdp-verifier"; +import { arrayContains, extractMetadataValue, getProofSetEntityId, getRootEntityId } from "./helpers"; import { DataSetStatus } from "./types"; -// ---- Helpers -------------------------------------------------------------- - -function getProofSetEntityId(setId: BigInt): Bytes { - return Bytes.fromByteArray(Bytes.fromBigInt(setId)); -} - -function arrayContains(arr: string[], needle: string): boolean { - for (let i = 0; i < arr.length; i++) { - if (arr[i] == needle) return true; - } - return false; -} - -function extractMetadataValue(keys: string[], values: string[], needle: string): string | null { - for (let i = 0; i < keys.length; i++) { - if (keys[i] == needle) { - return i < values.length ? values[i] : null; - } - } - return null; -} - // ---- Handlers ------------------------------------------------------------- export function handleFwssDataSetCreated(event: DataSetCreatedEvent): void { @@ -54,6 +32,7 @@ export function handleFwssDataSetCreated(event: DataSetCreatedEvent): void { ds.nextDeadline = BigInt.zero(); ds.maxProvingPeriod = BigInt.zero(); ds.provenThisPeriod = false; + ds.createdAt = event.block.timestamp; } ds.fwssPayer = event.params.payer; diff --git a/apps/subgraph/utils/cid.ts b/apps/subgraph/src/helpers.ts similarity index 58% rename from apps/subgraph/utils/cid.ts rename to apps/subgraph/src/helpers.ts index 95a577d5..01e0c8f6 100644 --- a/apps/subgraph/utils/cid.ts +++ b/apps/subgraph/src/helpers.ts @@ -1,4 +1,54 @@ -import { Bytes, BigInt } from "@graphprotocol/graph-ts"; +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; + +// ---- Entity ID helpers ---------------------------------------------------- + +export function getProofSetEntityId(setId: BigInt): Bytes { + return Bytes.fromByteArray(Bytes.fromBigInt(setId)); +} + +export function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { + return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); +} + +// ---- FWSS metadata helpers ------------------------------------------------ + +export function arrayContains(arr: string[], needle: string): boolean { + for (let i = 0; i < arr.length; i++) { + if (arr[i] == needle) return true; + } + return false; +} + +export function extractMetadataValue(keys: string[], values: string[], needle: string): string | null { + for (let i = 0; i < keys.length; i++) { + if (keys[i] == needle) { + return i < values.length ? values[i] : null; + } + } + return null; +} + +// ---- Per-network proving period ------------------------------------------ +// +// NextProvingPeriod is emitted by the PDPVerifier, so event.address on that +// handler is the PDPVerifier contract. Each subgraph build targets a single +// network, so only the matching branch is live for a given deployment — the +// others are dead code on that build, kept explicit here so the mapping is +// discoverable in one place rather than hidden behind a build-time constant. +// mainnet: MaxProvingPeriod = 2880 +// calibration: MaxProvingPeriod = 240 + +const MAINNET_PDP_VERIFIER = Address.fromString("0xBADd0B92C1c71d02E7d520f64c0876538fa2557F"); +const CALIBRATION_PDP_VERIFIER = Address.fromString("0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C"); + +export function maxProvingPeriodFor(pdpVerifier: Address): i32 { + if (pdpVerifier.equals(MAINNET_PDP_VERIFIER)) return 2880; + if (pdpVerifier.equals(CALIBRATION_PDP_VERIFIER)) return 240; + // Conservative fallback for unknown deployments (matches calibration). + return 240; +} + +// ---- CommP v2 CID decoding ------------------------------------------------ export const COMMP_V2_PREFIX: u8[] = [0x01, 0x55, 0x91, 0x20]; diff --git a/apps/subgraph/src/pdp-verifier.ts b/apps/subgraph/src/pdp-verifier.ts index b7c1547e..65f0f710 100644 --- a/apps/subgraph/src/pdp-verifier.ts +++ b/apps/subgraph/src/pdp-verifier.ts @@ -1,4 +1,4 @@ -import { BigInt, Bytes, log } from "@graphprotocol/graph-ts"; +import { BigInt, log } from "@graphprotocol/graph-ts"; import { DataSetCreated as DataSetCreatedEvent, DataSetDeleted as DataSetDeletedEvent, @@ -10,19 +10,14 @@ import { StorageProviderChanged as StorageProviderChangedEvent, } from "../generated/PDPVerifier/PDPVerifier"; import { DataSet, Provider, Root } from "../generated/schema"; +import { + getProofSetEntityId, + getRootEntityId, + maxProvingPeriodFor, + unpaddedSize, + validateCommPv2, +} from "./helpers"; import { DataSetStatus } from "./types"; -import { MaxProvingPeriod } from "../utils"; -import { unpaddedSize, validateCommPv2 } from "../utils/cid"; - -// ---- Entity ID helpers ---------------------------------------------------- - -function getProofSetEntityId(setId: BigInt): Bytes { - return Bytes.fromByteArray(Bytes.fromBigInt(setId)); -} - -export function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { - return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); -} // ---- Handlers ------------------------------------------------------------- @@ -39,6 +34,7 @@ export function handleDataSetCreated(event: DataSetCreatedEvent): void { if (proofSet == null) { proofSet = new DataSet(proofSetEntityId); proofSet.withIPFSIndexing = false; + proofSet.createdAt = event.block.timestamp; // fwssPayer, fwssServiceProvider, pdpPaymentEndEpoch are nullable. } proofSet.setId = event.params.setId; @@ -140,7 +136,7 @@ export function handleNextProvingPeriod(event: NextProvingPeriodEvent): void { if (proofSet.nextDeadline.equals(BigInt.zero())) { // First-init: promote to PROVING, seed maxProvingPeriod. proofSet.status = DataSetStatus.PROVING; - proofSet.maxProvingPeriod = BigInt.fromI32(MaxProvingPeriod); + proofSet.maxProvingPeriod = BigInt.fromI32(maxProvingPeriodFor(event.address)); nextDeadline = currentBlockNumber.plus(proofSet.maxProvingPeriod); } else { if (currentBlockNumber.gt(proofSet.nextDeadline)) { @@ -202,6 +198,7 @@ export function handlePiecesAdded(event: PiecesAddedEvent): void { root.rawSize = rawSize; root.cid = pieceBytes; root.removed = false; + root.createdAt = event.block.timestamp; root.proofSet = getProofSetEntityId(setId); // ipfsRootCID: patched in FWSS handler if applicable. root.save(); diff --git a/apps/subgraph/subgraph.yaml b/apps/subgraph/subgraph.yaml index aa4faf76..6f36ecdb 100644 --- a/apps/subgraph/subgraph.yaml +++ b/apps/subgraph/subgraph.yaml @@ -59,13 +59,16 @@ dataSources: - name: FilecoinWarmStorageService file: ./abis/FilecoinWarmStorageService.json eventHandlers: - - event: DataSetCreated(indexed uint256,indexed uint256,uint256,uint256,uint256,address,address,address,string[],string[]) + - event: DataSetCreated(indexed uint256,indexed + uint256,uint256,uint256,uint256,address,address,address,string[],string[]) handler: handleFwssDataSetCreated - event: PieceAdded(indexed uint256,indexed uint256,(bytes),string[],string[]) handler: handleFwssPieceAdded - - event: ServiceTerminated(indexed address,indexed uint256,uint256,uint256,uint256) + - event: ServiceTerminated(indexed address,indexed + uint256,uint256,uint256,uint256) handler: handleFwssServiceTerminated - event: PDPPaymentTerminated(indexed uint256,uint256,uint256) handler: handleFwssPdpPaymentTerminated - - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed address) + - event: DataSetServiceProviderChanged(indexed uint256,indexed address,indexed + address) handler: handleFwssDataSetServiceProviderChanged diff --git a/apps/subgraph/tests/fwss.test.ts b/apps/subgraph/tests/fwss.test.ts index ce359d1b..d63d7db4 100644 --- a/apps/subgraph/tests/fwss.test.ts +++ b/apps/subgraph/tests/fwss.test.ts @@ -1,6 +1,5 @@ import { assert, beforeEach, clearStore, describe, test } from "matchstick-as/assembly/index"; import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; -import { getRootEntityId, handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; import { handleFwssDataSetCreated, handleFwssDataSetServiceProviderChanged, @@ -8,6 +7,8 @@ import { handleFwssPieceAdded, handleFwssServiceTerminated, } from "../src/fwss"; +import { getRootEntityId } from "../src/helpers"; +import { handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; import { createDataSetCreatedEvent, createRootsAddedEvent } from "./pdp-verifier-utils"; import { createFwssDataSetCreatedEvent, diff --git a/apps/subgraph/tests/pdp-verifier.test.ts b/apps/subgraph/tests/pdp-verifier.test.ts index 805c306b..444adc32 100644 --- a/apps/subgraph/tests/pdp-verifier.test.ts +++ b/apps/subgraph/tests/pdp-verifier.test.ts @@ -1,6 +1,7 @@ import { afterAll, assert, beforeAll, clearStore, describe, test } from "matchstick-as/assembly/index"; import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; -import { getRootEntityId, handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; +import { getRootEntityId } from "../src/helpers"; +import { handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; import { createDataSetCreatedEvent, createRootsAddedEvent } from "./pdp-verifier-utils"; const SET_ID = BigInt.fromI32(1); diff --git a/apps/subgraph/utils/index.ts b/apps/subgraph/utils/index.ts deleted file mode 100644 index 38153b9b..00000000 --- a/apps/subgraph/utils/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Default proving period length in blocks. Applied when a DataSet enters -// its first NextProvingPeriod. The FWSS contract sets the real value -// on-chain; this is a conservative default for subgraph state init. -// calibration: MaxProvingPeriod = 240 -// mainnet: MaxProvingPeriod = 2880 -export const MaxProvingPeriod = 240; From 713bd96ff1675fa4ab2d0474c59f088081801cf0 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 11:40:55 +0200 Subject: [PATCH 08/19] rename: pdp-subgraph to just subgraph --- apps/backend/.env.example | 2 +- apps/backend/README.md | 2 +- apps/backend/src/config/app.config.ts | 6 +- .../data-retention/data-retention.module.ts | 4 +- .../data-retention.service.spec.ts | 152 +++++++++--------- .../data-retention/data-retention.service.ts | 16 +- .../src/pdp-subgraph/pdp-subgraph.module.ts | 8 - .../anon-piece-selector.service.spec.ts | 8 +- .../anon-piece-selector.service.ts | 8 +- .../retrieval-anon/retrieval-anon.module.ts | 4 +- .../src/{pdp-subgraph => subgraph}/queries.ts | 0 apps/backend/src/subgraph/subgraph.module.ts | 8 + .../subgraph.service.spec.ts} | 18 +-- .../subgraph.service.ts} | 72 ++++----- .../{pdp-subgraph => subgraph}/types.spec.ts | 0 .../src/{pdp-subgraph => subgraph}/types.ts | 0 .../src/wallet-sdk/wallet-sdk.service.spec.ts | 2 +- apps/subgraph/README.md | 6 +- docs/checks/data-retention.md | 10 +- ...-configuration-and-approval-methodology.md | 2 +- docs/environment-variables.md | 6 +- .../local/backend-configmap-local.yaml | 2 +- 22 files changed, 168 insertions(+), 168 deletions(-) delete mode 100644 apps/backend/src/pdp-subgraph/pdp-subgraph.module.ts rename apps/backend/src/{pdp-subgraph => subgraph}/queries.ts (100%) create mode 100644 apps/backend/src/subgraph/subgraph.module.ts rename apps/backend/src/{pdp-subgraph/pdp-subgraph.service.spec.ts => subgraph/subgraph.service.spec.ts} (98%) rename apps/backend/src/{pdp-subgraph/pdp-subgraph.service.ts => subgraph/subgraph.service.ts} (84%) rename apps/backend/src/{pdp-subgraph => subgraph}/types.spec.ts (100%) rename apps/backend/src/{pdp-subgraph => subgraph}/types.ts (100%) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index dc1f3500..eb6552b1 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -24,7 +24,7 @@ WALLET_PRIVATE_KEY=your_private_key_here CHECK_DATASET_CREATION_FEES=true USE_ONLY_APPROVED_PROVIDERS=true # Point at the dealbot-owned subgraph on Goldsky (see apps/subgraph/README.md). -PDP_SUBGRAPH_ENDPOINT=https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn +SUBGRAPH_ENDPOINT=https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn # Minimum number of datasets per SP (default: 1). When > 1, a separate data_set_creation job provisions extra datasets. MIN_NUM_DATASETS_FOR_CHECKS=1 diff --git a/apps/backend/README.md b/apps/backend/README.md index 19ee970a..4805080f 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -104,7 +104,7 @@ All configuration is done via environment variables in `.env`. | `CHECK_DATASET_CREATION_FEES` | Check fees before dataset creation | `true` | | `ENABLE_IPNI_TESTING` | IPNI testing mode (`disabled`/`random`/`always`) | `always` | | `USE_ONLY_APPROVED_PROVIDERS` | Only use approved storage providers | `true` | -| `PDP_SUBGRAPH_ENDPOINT` | PDP subgraph API endpoint for PDP proof-set/data-retention | `https://api.thegraph.com/subgraphs/filecoin/pdp` | +| `SUBGRAPH_ENDPOINT` | Subgraph GraphQL endpoint for PDP proof-set/data-retention and anon-retrieval queries | `https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn` | ### Scheduling Configuration (pg-boss) diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts index 92e01554..7b11becd 100644 --- a/apps/backend/src/config/app.config.ts +++ b/apps/backend/src/config/app.config.ts @@ -56,7 +56,7 @@ export const configValidationSchema = Joi.object({ USE_ONLY_APPROVED_PROVIDERS: Joi.boolean().default(true), DEALBOT_DATASET_VERSION: Joi.string().optional(), MIN_NUM_DATASETS_FOR_CHECKS: Joi.number().integer().min(1).default(1), - PDP_SUBGRAPH_ENDPOINT: Joi.string().uri().optional().allow(""), + SUBGRAPH_ENDPOINT: Joi.string().uri().optional().allow(""), // Scheduling PROVIDERS_REFRESH_INTERVAL_SECONDS: Joi.number().default(4 * 3600), @@ -167,7 +167,7 @@ export interface IBlockchainConfig { useOnlyApprovedProviders: boolean; dealbotDataSetVersion?: string; minNumDataSetsForChecks: number; - pdpSubgraphEndpoint?: string; + subgraphEndpoint?: string; } export interface ISchedulingConfig { @@ -368,7 +368,7 @@ export function loadConfig(): IConfig { useOnlyApprovedProviders: process.env.USE_ONLY_APPROVED_PROVIDERS !== "false", dealbotDataSetVersion: process.env.DEALBOT_DATASET_VERSION, minNumDataSetsForChecks: Number.parseInt(process.env.MIN_NUM_DATASETS_FOR_CHECKS || "1", 10), - pdpSubgraphEndpoint: process.env.PDP_SUBGRAPH_ENDPOINT || "", + subgraphEndpoint: process.env.SUBGRAPH_ENDPOINT || "", }, scheduling: { providersRefreshIntervalSeconds: Number.parseInt(process.env.PROVIDERS_REFRESH_INTERVAL_SECONDS || "14400", 10), diff --git a/apps/backend/src/data-retention/data-retention.module.ts b/apps/backend/src/data-retention/data-retention.module.ts index f459570a..f0aec1ec 100644 --- a/apps/backend/src/data-retention/data-retention.module.ts +++ b/apps/backend/src/data-retention/data-retention.module.ts @@ -2,12 +2,12 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { DataRetentionBaseline } from "../database/entities/data-retention-baseline.entity.js"; import { StorageProvider } from "../database/entities/storage-provider.entity.js"; -import { PdpSubgraphModule } from "../pdp-subgraph/pdp-subgraph.module.js"; +import { SubgraphModule } from "../subgraph/subgraph.module.js"; import { WalletSdkModule } from "../wallet-sdk/wallet-sdk.module.js"; import { DataRetentionService } from "./data-retention.service.js"; @Module({ - imports: [WalletSdkModule, PdpSubgraphModule, TypeOrmModule.forFeature([DataRetentionBaseline, StorageProvider])], + imports: [WalletSdkModule, SubgraphModule, TypeOrmModule.forFeature([DataRetentionBaseline, StorageProvider])], providers: [DataRetentionService], exports: [DataRetentionService], }) diff --git a/apps/backend/src/data-retention/data-retention.service.spec.ts b/apps/backend/src/data-retention/data-retention.service.spec.ts index 17151bd1..254a9707 100644 --- a/apps/backend/src/data-retention/data-retention.service.spec.ts +++ b/apps/backend/src/data-retention/data-retention.service.spec.ts @@ -6,8 +6,8 @@ import type { IConfig } from "../config/app.config.js"; import type { DataRetentionBaseline } from "../database/entities/data-retention-baseline.entity.js"; import { StorageProvider } from "../database/entities/storage-provider.entity.js"; import { buildCheckMetricLabels } from "../metrics-prometheus/check-metric-labels.js"; -import type { PDPSubgraphService } from "../pdp-subgraph/pdp-subgraph.service.js"; -import type { ProviderDataSetResponse } from "../pdp-subgraph/types.js"; +import type { SubgraphService } from "../subgraph/subgraph.service.js"; +import type { ProviderDataSetResponse } from "../subgraph/types.js"; import type { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; import { DataRetentionService } from "./data-retention.service.js"; @@ -35,7 +35,7 @@ describe("DataRetentionService", () => { let walletSdkServiceMock: { getTestingProviders: ReturnType; }; - let pdpSubgraphServiceMock: { + let subgraphServiceMock: { fetchSubgraphMeta: ReturnType; fetchProvidersWithDatasets: ReturnType; }; @@ -62,7 +62,7 @@ describe("DataRetentionService", () => { configServiceMock = { get: vi.fn((key: keyof IConfig) => { if (key === "blockchain") { - return { pdpSubgraphEndpoint: "https://example.com/subgraph" }; + return { subgraphEndpoint: "https://example.com/subgraph" }; } if (key === "spBlocklists") { return { ids: new Set(), addresses: new Set() }; @@ -88,7 +88,7 @@ describe("DataRetentionService", () => { ]), }; - pdpSubgraphServiceMock = { + subgraphServiceMock = { fetchSubgraphMeta: vi.fn().mockResolvedValue({ _meta: { block: { @@ -124,7 +124,7 @@ describe("DataRetentionService", () => { service = new DataRetentionService( configServiceMock, walletSdkServiceMock as unknown as WalletSdkService, - pdpSubgraphServiceMock as unknown as PDPSubgraphService, + subgraphServiceMock as unknown as SubgraphService, mockBaselineRepository as unknown as Repository, mockSPRepository as unknown as Repository, counterMock as unknown as Counter, @@ -132,15 +132,15 @@ describe("DataRetentionService", () => { ); }); - it("returns early when pdpSubgraphEndpoint is empty", async () => { + it("returns early when subgraphEndpoint is empty", async () => { (configServiceMock.get as ReturnType).mockReturnValue({ - pdpSubgraphEndpoint: "", + subgraphEndpoint: "", }); await service.pollDataRetention(); - expect(pdpSubgraphServiceMock.fetchSubgraphMeta).not.toHaveBeenCalled(); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); + expect(subgraphServiceMock.fetchSubgraphMeta).not.toHaveBeenCalled(); + expect(subgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); }); it("returns early when no testing providers configured", async () => { @@ -148,7 +148,7 @@ describe("DataRetentionService", () => { await service.pollDataRetention(); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); + expect(subgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); }); it("returns early when all providers are blocked for data-retention", async () => { @@ -183,16 +183,16 @@ describe("DataRetentionService", () => { await service.pollDataRetention(); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); + expect(subgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); }); it("sets baseline on first poll without emitting counters (fresh deploy / new provider)", async () => { - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); - expect(pdpSubgraphServiceMock.fetchSubgraphMeta).toHaveBeenCalled(); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenCalledWith({ + expect(subgraphServiceMock.fetchSubgraphMeta).toHaveBeenCalled(); + expect(subgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenCalledWith({ blockNumber: 1200, addresses: [PROVIDER_A, PROVIDER_B], }); @@ -216,20 +216,20 @@ describe("DataRetentionService", () => { it("computes deltas correctly on consecutive polls", async () => { // First poll: blockNumber=1200 - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); const firstCallCount = counterMock.labels.mock.calls.length; // Second poll: blockNumber=1300, provider totals changed - pdpSubgraphServiceMock.fetchSubgraphMeta.mockResolvedValueOnce({ + subgraphServiceMock.fetchSubgraphMeta.mockResolvedValueOnce({ _meta: { block: { number: 1300, }, }, }); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 12n, totalProvingPeriods: 105n, @@ -243,7 +243,7 @@ describe("DataRetentionService", () => { }); it("does not increment counters when deltas are zero", async () => { - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([makeProvider()]); // First poll await service.pollDataRetention(); @@ -265,7 +265,7 @@ describe("DataRetentionService", () => { const providerA = makeProvider({ address: PROVIDER_A, totalFaultedPeriods: 5n }); const providerB = makeProvider({ address: PROVIDER_B, totalFaultedPeriods: 20n }); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([providerA, providerB]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([providerA, providerB]); await service.pollDataRetention(); @@ -287,7 +287,7 @@ describe("DataRetentionService", () => { ]); const provider = makeProvider(); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([provider]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([provider]); await service.pollDataRetention(); @@ -310,7 +310,7 @@ describe("DataRetentionService", () => { }); it("handles empty providers array without errors", async () => { - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([]); await service.pollDataRetention(); @@ -324,7 +324,7 @@ describe("DataRetentionService", () => { ]); const provider = makeProvider(); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([provider]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([provider]); await service.pollDataRetention(); @@ -347,7 +347,7 @@ describe("DataRetentionService", () => { }); it("catches and logs errors without rethrowing", async () => { - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockRejectedValueOnce(new Error("subgraph down")); + subgraphServiceMock.fetchProvidersWithDatasets.mockRejectedValueOnce(new Error("subgraph down")); // Should not throw await expect(service.pollDataRetention()).resolves.toBeUndefined(); @@ -355,14 +355,14 @@ describe("DataRetentionService", () => { it("resets baseline on negative deltas without incrementing counters", async () => { // First poll: high values - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 100n, totalProvingPeriods: 200n }), ]); await service.pollDataRetention(); counterMock.labels.mockClear(); // Second poll: lower values (e.g., chain reorg or subgraph correction) - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 50n, totalProvingPeriods: 100n }), ]); await service.pollDataRetention(); @@ -371,7 +371,7 @@ describe("DataRetentionService", () => { expect(counterMock.labels).not.toHaveBeenCalled(); // Third poll: values increase from new baseline - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 52n, totalProvingPeriods: 105n }), ]); await service.pollDataRetention(); @@ -389,7 +389,7 @@ describe("DataRetentionService", () => { { providerAddress: PROVIDER_A, faultedPeriods: "0", successPeriods: "0", lastBlockNumber: "1000" }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: largeValue, totalProvingPeriods: largeValue * 2n }), ]); @@ -413,7 +413,7 @@ describe("DataRetentionService", () => { { providerAddress: PROVIDER_A, faultedPeriods: "0", successPeriods: "0", lastBlockNumber: "1000" }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: maxSafeInt, totalProvingPeriods: maxSafeInt * 2n }), ]); @@ -433,7 +433,7 @@ describe("DataRetentionService", () => { totalFaultedPeriods: 5n, totalProvingPeriods: 50n, }); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([provider]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([provider]); await service.pollDataRetention(); @@ -452,18 +452,18 @@ describe("DataRetentionService", () => { })); walletSdkServiceMock.getTestingProviders.mockReturnValueOnce(manyProviders); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([]); await service.pollDataRetention(); // Should be called twice: once for first 50, once for remaining 25 - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenCalledTimes(2); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenNthCalledWith(1, { + expect(subgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenCalledTimes(2); + expect(subgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenNthCalledWith(1, { addresses: expect.arrayContaining([expect.any(String)]), blockNumber: 1200, }); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets.mock.calls[0][0].addresses).toHaveLength(50); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets.mock.calls[1][0].addresses).toHaveLength(25); + expect(subgraphServiceMock.fetchProvidersWithDatasets.mock.calls[0][0].addresses).toHaveLength(50); + expect(subgraphServiceMock.fetchProvidersWithDatasets.mock.calls[1][0].addresses).toHaveLength(25); }); it("continues processing next batch if one batch fails", async () => { @@ -476,20 +476,20 @@ describe("DataRetentionService", () => { walletSdkServiceMock.getTestingProviders.mockReturnValueOnce(manyProviders); // First batch fails, second succeeds - pdpSubgraphServiceMock.fetchProvidersWithDatasets + subgraphServiceMock.fetchProvidersWithDatasets .mockRejectedValueOnce(new Error("Subgraph timeout")) .mockResolvedValueOnce([]); await service.pollDataRetention(); // Both batches should be attempted - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenCalledTimes(2); + expect(subgraphServiceMock.fetchProvidersWithDatasets).toHaveBeenCalledTimes(2); }); it("logs error and skips counter update when provider not found in cache but returned from subgraph", async () => { // Provider C not in cache const PROVIDER_C = "0x1234567890123456789012345678901234567890"; - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_C })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_C })]); await service.pollDataRetention(); @@ -500,7 +500,7 @@ describe("DataRetentionService", () => { describe("cleanupStaleProviders", () => { it("does not cleanup when no stale providers exist", async () => { // First poll establishes baseline for both providers - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ address: PROVIDER_A }), makeProvider({ address: PROVIDER_B }), ]); @@ -513,7 +513,7 @@ describe("DataRetentionService", () => { it("successfully cleans up stale provider with valid database entry", async () => { // First poll: establish baseline for PROVIDER_A - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: PROVIDER_A removed from active list, only PROVIDER_B active @@ -535,7 +535,7 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -566,7 +566,7 @@ describe("DataRetentionService", () => { it("skips cleanup entirely when database fetch fails", async () => { // First poll: establish baseline - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: provider removed, but DB fails @@ -581,7 +581,7 @@ describe("DataRetentionService", () => { mockSPRepository.find.mockRejectedValueOnce(new Error("Database connection failed")); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -601,7 +601,7 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ address: PROVIDER_A, totalFaultedPeriods: 12n, totalProvingPeriods: 105n }), ]); @@ -614,7 +614,7 @@ describe("DataRetentionService", () => { it("retains baseline when provider not found in database", async () => { // First poll: establish baseline - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: provider removed from active list @@ -630,7 +630,7 @@ describe("DataRetentionService", () => { // Database returns empty array (provider not found) mockSPRepository.find.mockResolvedValueOnce([]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -647,7 +647,7 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ address: PROVIDER_A, totalFaultedPeriods: 12n, totalProvingPeriods: 105n }), ]); @@ -660,7 +660,7 @@ describe("DataRetentionService", () => { it("retains baseline when provider has null providerId", async () => { // First poll: establish baseline - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: provider removed @@ -683,7 +683,7 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -693,7 +693,7 @@ describe("DataRetentionService", () => { it("retains baseline when counter removal throws error", async () => { // First poll: establish baseline - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: provider removed @@ -720,7 +720,7 @@ describe("DataRetentionService", () => { throw new Error("Counter removal failed"); }); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -737,7 +737,7 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ address: PROVIDER_A, totalFaultedPeriods: 12n, totalProvingPeriods: 110n }), ]); @@ -758,7 +758,7 @@ describe("DataRetentionService", () => { { id: 3, serviceProvider: PROVIDER_C, name: "Provider C", isApproved: true }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ address: PROVIDER_A }), makeProvider({ address: PROVIDER_B }), makeProvider({ address: PROVIDER_C }), @@ -776,7 +776,7 @@ describe("DataRetentionService", () => { { address: PROVIDER_C, name: "Provider C", providerId: 3, isApproved: true }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); @@ -792,7 +792,7 @@ describe("DataRetentionService", () => { it("skips cleanup when processing errors occurred", async () => { // First poll: establish baseline - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: provider removed, but processing has errors @@ -801,7 +801,7 @@ describe("DataRetentionService", () => { ]); // Simulate processing error - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockRejectedValueOnce(new Error("Processing failed")); + subgraphServiceMock.fetchProvidersWithDatasets.mockRejectedValueOnce(new Error("Processing failed")); await service.pollDataRetention(); @@ -818,7 +818,7 @@ describe("DataRetentionService", () => { { id: 1, serviceProvider: PROVIDER_MIXED_CASE, name: "Provider A", isApproved: true }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ address: PROVIDER_MIXED_CASE.toLowerCase() as `0x${string}` }), ]); @@ -838,7 +838,7 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -862,7 +862,7 @@ describe("DataRetentionService", () => { // Subgraph returns same values: totalFaultedPeriods=10, totalProvingPeriods=100 // confirmedTotalSuccess = 100 - 10 = 90 // With DB baseline: faultedDelta = 10 - 10 = 0, successDelta = 90 - 90 = 0 - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); @@ -884,7 +884,7 @@ describe("DataRetentionService", () => { // Subgraph returns: totalFaultedPeriods=10, totalProvingPeriods=100 // confirmedTotalSuccess = 100 - 10 = 90 // faultedDelta = 10 - 8 = 2, successDelta = 90 - 85 = 5 - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); @@ -899,7 +899,7 @@ describe("DataRetentionService", () => { }); it("only loads baselines from DB once across multiple polls", async () => { - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([makeProvider()]); await service.pollDataRetention(); await service.pollDataRetention(); @@ -919,12 +919,12 @@ describe("DataRetentionService", () => { }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValue([makeProvider()]); // First poll: DB load fails, poll bails out to avoid emitting bloated values await service.pollDataRetention(); expect(mockBaselineRepository.find).toHaveBeenCalledTimes(1); - expect(pdpSubgraphServiceMock.fetchSubgraphMeta).not.toHaveBeenCalled(); + expect(subgraphServiceMock.fetchSubgraphMeta).not.toHaveBeenCalled(); expect(counterMock.labels).not.toHaveBeenCalled(); // Second poll: DB load succeeds, baselines restored, normal delta computation @@ -937,16 +937,16 @@ describe("DataRetentionService", () => { it("emits real deltas on second poll after fresh deploy baseline-only first poll", async () => { // First poll: fresh deploy, no baselines in DB // Baseline set to: faultedPeriods=10, successPeriods=90 - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); counterMock.labels.mockClear(); counterMock.inc.mockClear(); // Second poll: values have increased - pdpSubgraphServiceMock.fetchSubgraphMeta.mockResolvedValueOnce({ + subgraphServiceMock.fetchSubgraphMeta.mockResolvedValueOnce({ _meta: { block: { number: 1300 } }, }); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 12n, totalProvingPeriods: 105n }), ]); @@ -960,7 +960,7 @@ describe("DataRetentionService", () => { it("deletes baseline from DB when stale provider is cleaned up", async () => { // First poll: establish baseline for PROVIDER_A - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: PROVIDER_A removed from active list @@ -972,7 +972,7 @@ describe("DataRetentionService", () => { { address: PROVIDER_A, name: "Provider A", providerId: 1, isApproved: true }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); @@ -985,7 +985,7 @@ describe("DataRetentionService", () => { it("emits overdue gauge on first poll (baseline-only)", async () => { // Provider is overdue: currentBlock=1200, // estimatedOverduePeriods = (1200 - 901) / 100 = 2.99 -> 2 - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); @@ -1002,7 +1002,7 @@ describe("DataRetentionService", () => { it("emits overdue gauge = 0 when provider is not overdue", async () => { // nextDeadline=2000 > currentBlock=1200 - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ proofSets: [] })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ proofSets: [] })]); await service.pollDataRetention(); @@ -1011,7 +1011,7 @@ describe("DataRetentionService", () => { it("emits overdue gauge even on negative delta (baseline reset)", async () => { // First poll: high values - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 100n, totalProvingPeriods: 200n }), ]); await service.pollDataRetention(); @@ -1019,7 +1019,7 @@ describe("DataRetentionService", () => { gaugeMock.set.mockClear(); // Second poll: lower values (negative delta) but still overdue - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 50n, totalProvingPeriods: 100n }), ]); await service.pollDataRetention(); @@ -1031,7 +1031,7 @@ describe("DataRetentionService", () => { it("naturally resets gauge to 0 when subgraph catches up", async () => { // First poll: provider is overdue (currentBlock=1200, nextDeadline=1000) - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider()]); await service.pollDataRetention(); expect(gaugeMock.set).toHaveBeenCalledWith(2); @@ -1040,7 +1040,7 @@ describe("DataRetentionService", () => { gaugeMock.set.mockClear(); // Second poll: subgraph caught up, nextDeadline advanced past currentBlock - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([ makeProvider({ totalFaultedPeriods: 12n, totalProvingPeriods: 102n, @@ -1056,7 +1056,7 @@ describe("DataRetentionService", () => { it("removes overdue gauge when stale provider is cleaned up", async () => { // First poll: establish baseline for PROVIDER_A - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_A })]); await service.pollDataRetention(); // Second poll: PROVIDER_A removed from active list @@ -1068,7 +1068,7 @@ describe("DataRetentionService", () => { { address: PROVIDER_A, name: "Provider A", providerId: 1, isApproved: true }, ]); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); diff --git a/apps/backend/src/data-retention/data-retention.service.ts b/apps/backend/src/data-retention/data-retention.service.ts index f4d7ec6d..16f87d41 100644 --- a/apps/backend/src/data-retention/data-retention.service.ts +++ b/apps/backend/src/data-retention/data-retention.service.ts @@ -10,8 +10,8 @@ import { IConfig } from "../config/app.config.js"; import { DataRetentionBaseline } from "../database/entities/data-retention-baseline.entity.js"; import { StorageProvider } from "../database/entities/storage-provider.entity.js"; import { buildCheckMetricLabels, CheckMetricLabels } from "../metrics-prometheus/check-metric-labels.js"; -import { PDPSubgraphService } from "../pdp-subgraph/pdp-subgraph.service.js"; -import { type ProviderDataSetResponse } from "../pdp-subgraph/types.js"; +import { SubgraphService } from "../subgraph/subgraph.service.js"; +import { type ProviderDataSetResponse } from "../subgraph/types.js"; import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; import { type PDPProviderEx } from "../wallet-sdk/wallet-sdk.types.js"; @@ -44,7 +44,7 @@ export class DataRetentionService { constructor( private readonly configService: ConfigService, private readonly walletSdkService: WalletSdkService, - private readonly pdpSubgraphService: PDPSubgraphService, + private readonly subgraphService: SubgraphService, @InjectRepository(DataRetentionBaseline) private readonly baselineRepository: Repository, @InjectRepository(StorageProvider) @@ -63,10 +63,10 @@ export class DataRetentionService { * challenge delta since the last poll. */ async pollDataRetention(): Promise { - const pdpSubgraphEndpoint = this.configService.get("blockchain").pdpSubgraphEndpoint; - if (!pdpSubgraphEndpoint) { + const subgraphEndpoint = this.configService.get("blockchain").subgraphEndpoint; + if (!subgraphEndpoint) { this.logger.warn({ - event: "pdp_subgraph_endpoint_not_configured", + event: "subgraph_endpoint_not_configured", message: "No PDP subgraph endpoint configured", }); return; @@ -80,7 +80,7 @@ export class DataRetentionService { } try { - const subgraphMeta = await this.pdpSubgraphService.fetchSubgraphMeta(); + const subgraphMeta = await this.subgraphService.fetchSubgraphMeta(); const allProviderInfos = this.walletSdkService.getTestingProviders(); const spBlocklists = this.configService.get("spBlocklists"); const providerInfos = allProviderInfos?.filter((p) => !isSpBlocked(spBlocklists, p.serviceProvider, p.id)); @@ -109,7 +109,7 @@ export class DataRetentionService { ); try { - const providersFromSubgraph = await this.pdpSubgraphService.fetchProvidersWithDatasets({ + const providersFromSubgraph = await this.subgraphService.fetchProvidersWithDatasets({ blockNumber, addresses: batchAddresses, }); diff --git a/apps/backend/src/pdp-subgraph/pdp-subgraph.module.ts b/apps/backend/src/pdp-subgraph/pdp-subgraph.module.ts deleted file mode 100644 index 6e084fc1..00000000 --- a/apps/backend/src/pdp-subgraph/pdp-subgraph.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from "@nestjs/common"; -import { PDPSubgraphService } from "./pdp-subgraph.service.js"; - -@Module({ - providers: [PDPSubgraphService], - exports: [PDPSubgraphService], -}) -export class PdpSubgraphModule {} diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts index 306a6a0c..cf3a1cea 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts @@ -3,8 +3,8 @@ import type { Repository } from "typeorm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { IConfig } from "../config/app.config.js"; import type { Retrieval } from "../database/entities/retrieval.entity.js"; -import type { PDPSubgraphService } from "../pdp-subgraph/pdp-subgraph.service.js"; -import type { FwssCandidatePiece } from "../pdp-subgraph/types.js"; +import type { SubgraphService } from "../subgraph/subgraph.service.js"; +import type { FwssCandidatePiece } from "../subgraph/types.js"; import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; const SP_ADDRESS = "0xAaAaAAaAaaaAaAAAAaaaaAAaaAaaaAAaaaaa1111"; @@ -45,12 +45,12 @@ const makeConfigService = (): ConfigService => }) as unknown as ConfigService; describe("AnonPieceSelectorService", () => { - let subgraphService: PDPSubgraphService; + let subgraphService: SubgraphService; let listFwssCandidatePieces: ReturnType; beforeEach(() => { listFwssCandidatePieces = vi.fn(); - subgraphService = { listFwssCandidatePieces } as unknown as PDPSubgraphService; + subgraphService = { listFwssCandidatePieces } as unknown as SubgraphService; }); it("returns null when the subgraph yields no candidates", async () => { diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts index fc083014..8e4eadbd 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts @@ -4,8 +4,8 @@ import { InjectRepository } from "@nestjs/typeorm"; import type { Repository } from "typeorm"; import type { IConfig } from "../config/app.config.js"; import { Retrieval } from "../database/entities/retrieval.entity.js"; -import { PDPSubgraphService } from "../pdp-subgraph/pdp-subgraph.service.js"; -import type { FwssCandidatePiece } from "../pdp-subgraph/types.js"; +import { SubgraphService } from "../subgraph/subgraph.service.js"; +import type { FwssCandidatePiece } from "../subgraph/types.js"; import type { AnonPiece } from "./types.js"; /** @@ -21,7 +21,7 @@ export class AnonPieceSelectorService { private readonly logger = new Logger(AnonPieceSelectorService.name); constructor( - private readonly pdpSubgraphService: PDPSubgraphService, + private readonly subgraphService: SubgraphService, private readonly configService: ConfigService, @InjectRepository(Retrieval) private readonly retrievalRepository: Repository, @@ -37,7 +37,7 @@ export class AnonPieceSelectorService { */ async selectPieceForProvider(spAddress: string): Promise { const dealbotPayer = this.configService.get("blockchain", { infer: true }).walletAddress; - const candidates = await this.pdpSubgraphService.listFwssCandidatePieces(spAddress, dealbotPayer); + const candidates = await this.subgraphService.listFwssCandidatePieces(spAddress, dealbotPayer); if (candidates.length === 0) { this.logger.warn({ diff --git a/apps/backend/src/retrieval-anon/retrieval-anon.module.ts b/apps/backend/src/retrieval-anon/retrieval-anon.module.ts index ba799199..08210103 100644 --- a/apps/backend/src/retrieval-anon/retrieval-anon.module.ts +++ b/apps/backend/src/retrieval-anon/retrieval-anon.module.ts @@ -5,7 +5,7 @@ import { Retrieval } from "../database/entities/retrieval.entity.js"; import { StorageProvider } from "../database/entities/storage-provider.entity.js"; import { HttpClientModule } from "../http-client/http-client.module.js"; import { IpniModule } from "../ipni/ipni.module.js"; -import { PdpSubgraphModule } from "../pdp-subgraph/pdp-subgraph.module.js"; +import { SubgraphModule } from "../subgraph/subgraph.module.js"; import { WalletSdkModule } from "../wallet-sdk/wallet-sdk.module.js"; import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; import { AnonRetrievalService } from "./anon-retrieval.service.js"; @@ -16,7 +16,7 @@ import { PieceRetrievalService } from "./piece-retrieval.service.js"; imports: [ ConfigModule, TypeOrmModule.forFeature([Retrieval, StorageProvider]), - PdpSubgraphModule, + SubgraphModule, WalletSdkModule, HttpClientModule, IpniModule, diff --git a/apps/backend/src/pdp-subgraph/queries.ts b/apps/backend/src/subgraph/queries.ts similarity index 100% rename from apps/backend/src/pdp-subgraph/queries.ts rename to apps/backend/src/subgraph/queries.ts diff --git a/apps/backend/src/subgraph/subgraph.module.ts b/apps/backend/src/subgraph/subgraph.module.ts new file mode 100644 index 00000000..7834c39b --- /dev/null +++ b/apps/backend/src/subgraph/subgraph.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { SubgraphService } from "./subgraph.service.js"; + +@Module({ + providers: [SubgraphService], + exports: [SubgraphService], +}) +export class SubgraphModule {} diff --git a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts b/apps/backend/src/subgraph/subgraph.service.spec.ts similarity index 98% rename from apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts rename to apps/backend/src/subgraph/subgraph.service.spec.ts index 56696dae..529b1ba3 100644 --- a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.spec.ts +++ b/apps/backend/src/subgraph/subgraph.service.spec.ts @@ -2,7 +2,7 @@ import type { ConfigService } from "@nestjs/config"; import { CID } from "multiformats/cid"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { IConfig } from "../config/app.config.js"; -import { PDPSubgraphService } from "./pdp-subgraph.service.js"; +import { SubgraphService } from "./subgraph.service.js"; const VALID_ADDRESS = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" as const; const SUBGRAPH_ENDPOINT = "https://api.thegraph.com/subgraphs/filecoin/pdp" as const; @@ -63,21 +63,21 @@ const makeFwssDataSet = (overrides: Record = {}) => ({ ...overrides, }); -describe("PDPSubgraphService", () => { - let service: PDPSubgraphService; +describe("SubgraphService", () => { + let service: SubgraphService; let fetchMock: ReturnType; beforeEach(() => { const configService = { get: vi.fn((key: keyof IConfig) => { if (key === "blockchain") { - return { pdpSubgraphEndpoint: SUBGRAPH_ENDPOINT }; + return { subgraphEndpoint: SUBGRAPH_ENDPOINT }; } return undefined; }), } as unknown as ConfigService; - service = new PDPSubgraphService(configService); + service = new SubgraphService(configService); fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); @@ -390,10 +390,10 @@ describe("PDPSubgraphService", () => { it("throws when PDP subgraph endpoint is not configured", async () => { const configService = { - get: vi.fn(() => ({ pdpSubgraphEndpoint: "" })), + get: vi.fn(() => ({ subgraphEndpoint: "" })), } as unknown as ConfigService; - const serviceWithoutEndpoint = new PDPSubgraphService(configService); + const serviceWithoutEndpoint = new SubgraphService(configService); await expect(serviceWithoutEndpoint.fetchSubgraphMeta()).rejects.toThrow("No PDP subgraph endpoint configured"); }); @@ -723,9 +723,9 @@ describe("PDPSubgraphService", () => { describe("listFwssCandidatePieces", () => { it("returns empty array when endpoint is not configured", async () => { const noEndpointConfig = { - get: vi.fn(() => ({ pdpSubgraphEndpoint: "" })), + get: vi.fn(() => ({ subgraphEndpoint: "" })), } as unknown as ConfigService; - const noEndpointService = new PDPSubgraphService(noEndpointConfig); + const noEndpointService = new SubgraphService(noEndpointConfig); const pieces = await noEndpointService.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); expect(pieces).toEqual([]); diff --git a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts b/apps/backend/src/subgraph/subgraph.service.ts similarity index 84% rename from apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts rename to apps/backend/src/subgraph/subgraph.service.ts index 2b767de7..ef73d359 100644 --- a/apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts +++ b/apps/backend/src/subgraph/subgraph.service.ts @@ -33,8 +33,8 @@ class ValidationError extends Error { } @Injectable() -export class PDPSubgraphService { - private readonly logger: Logger = new Logger(PDPSubgraphService.name); +export class SubgraphService { + private readonly logger: Logger = new Logger(SubgraphService.name); private readonly blockchainConfig: IBlockchainConfig; private static readonly MAX_PROVIDERS_PER_QUERY = 100; @@ -62,14 +62,14 @@ export class PDPSubgraphService { * @throws Error if endpoint is not configured or after MAX_RETRIES attempts */ async fetchSubgraphMeta(attempt: number = 1): Promise { - if (!this.blockchainConfig.pdpSubgraphEndpoint) { + if (!this.blockchainConfig.subgraphEndpoint) { throw new Error("No PDP subgraph endpoint configured"); } try { await this.enforceRateLimit(); - const response = await fetch(this.blockchainConfig.pdpSubgraphEndpoint, { + const response = await fetch(this.blockchainConfig.subgraphEndpoint, { method: "POST", headers: { "Content-Type": "application/json", @@ -112,13 +112,13 @@ export class PDPSubgraphService { } // Retry on network/HTTP errors - if (attempt < PDPSubgraphService.MAX_RETRIES) { - const delay = PDPSubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); + if (attempt < SubgraphService.MAX_RETRIES) { + const delay = SubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); this.logger.warn({ event: "subgraph_meta_request_retry", message: "Subgraph meta request failed. Retrying...", attempt, - maxRetries: PDPSubgraphService.MAX_RETRIES, + maxRetries: SubgraphService.MAX_RETRIES, retryDelayMs: delay, error: toStructuredError(error), }); @@ -129,11 +129,11 @@ export class PDPSubgraphService { this.logger.error({ event: "subgraph_meta_request_failed", message: "Subgraph meta request failed after maximum retries", - maxRetries: PDPSubgraphService.MAX_RETRIES, + maxRetries: SubgraphService.MAX_RETRIES, error: toStructuredError(error), }); throw new Error( - `Failed to fetch subgraph metadata after ${PDPSubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, + `Failed to fetch subgraph metadata after ${SubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, ); } } @@ -153,7 +153,7 @@ export class PDPSubgraphService { return []; } - if (addresses.length <= PDPSubgraphService.MAX_PROVIDERS_PER_QUERY) { + if (addresses.length <= SubgraphService.MAX_PROVIDERS_PER_QUERY) { return this.fetchWithRetry(blockNumber, addresses); } @@ -171,15 +171,15 @@ export class PDPSubgraphService { * Returns an empty array if the subgraph endpoint is not configured. */ async listFwssCandidatePieces(spAddress: string, dealbotPayer: string): Promise { - if (!this.blockchainConfig.pdpSubgraphEndpoint) { + if (!this.blockchainConfig.subgraphEndpoint) { return []; } const variables = { serviceProvider: spAddress.toLowerCase(), payer: dealbotPayer.toLowerCase(), - datasetLimit: PDPSubgraphService.FWSS_DATASET_LIMIT, - pieceLimit: PDPSubgraphService.FWSS_PIECE_LIMIT, + datasetLimit: SubgraphService.FWSS_DATASET_LIMIT, + pieceLimit: SubgraphService.FWSS_PIECE_LIMIT, }; const validated = await this.executeQuery( @@ -237,14 +237,14 @@ export class PDPSubgraphService { transform: (data: unknown) => T, attempt: number = 1, ): Promise { - if (!this.blockchainConfig.pdpSubgraphEndpoint) { + if (!this.blockchainConfig.subgraphEndpoint) { throw new Error("No PDP subgraph endpoint configured"); } try { await this.enforceRateLimit(); - const response = await fetch(this.blockchainConfig.pdpSubgraphEndpoint, { + const response = await fetch(this.blockchainConfig.subgraphEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query, variables }), @@ -279,13 +279,13 @@ export class PDPSubgraphService { throw error; } - if (attempt < PDPSubgraphService.MAX_RETRIES) { - const delay = PDPSubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); + if (attempt < SubgraphService.MAX_RETRIES) { + const delay = SubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); this.logger.warn({ event: `subgraph_${operationName}_request_retry`, message: `Subgraph ${operationName} request failed. Retrying...`, attempt, - maxRetries: PDPSubgraphService.MAX_RETRIES, + maxRetries: SubgraphService.MAX_RETRIES, retryDelayMs: delay, error: toStructuredError(error), }); @@ -296,11 +296,11 @@ export class PDPSubgraphService { this.logger.error({ event: `subgraph_${operationName}_request_failed`, message: `Subgraph ${operationName} request failed after maximum retries`, - maxRetries: PDPSubgraphService.MAX_RETRIES, + maxRetries: SubgraphService.MAX_RETRIES, error: toStructuredError(error), }); throw new Error( - `Failed to fetch subgraph ${operationName} after ${PDPSubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, + `Failed to fetch subgraph ${operationName} after ${SubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, ); } } @@ -313,15 +313,15 @@ export class PDPSubgraphService { addresses: string[], ): Promise { const batches: string[][] = []; - for (let i = 0; i < addresses.length; i += PDPSubgraphService.MAX_PROVIDERS_PER_QUERY) { - const addressesLimit = Math.min(addresses.length, i + PDPSubgraphService.MAX_PROVIDERS_PER_QUERY); + for (let i = 0; i < addresses.length; i += SubgraphService.MAX_PROVIDERS_PER_QUERY) { + const addressesLimit = Math.min(addresses.length, i + SubgraphService.MAX_PROVIDERS_PER_QUERY); batches.push(addresses.slice(i, addressesLimit)); } const allProviders: ProviderDataSetResponse["providers"] = []; - for (let i = 0; i < batches.length; i += PDPSubgraphService.MAX_CONCURRENT_REQUESTS) { - const batchGroup = batches.slice(i, i + PDPSubgraphService.MAX_CONCURRENT_REQUESTS); + for (let i = 0; i < batches.length; i += SubgraphService.MAX_CONCURRENT_REQUESTS) { + const batchGroup = batches.slice(i, i + SubgraphService.MAX_CONCURRENT_REQUESTS); const results = await Promise.all(batchGroup.map((batch) => this.fetchWithRetry(blockNumber, batch))); @@ -340,7 +340,7 @@ export class PDPSubgraphService { addresses: string[], attempt: number = 1, ): Promise { - if (!this.blockchainConfig.pdpSubgraphEndpoint) { + if (!this.blockchainConfig.subgraphEndpoint) { throw new Error("No PDP subgraph endpoint configured"); } @@ -352,7 +352,7 @@ export class PDPSubgraphService { try { await this.enforceRateLimit(); - const response = await fetch(this.blockchainConfig.pdpSubgraphEndpoint, { + const response = await fetch(this.blockchainConfig.subgraphEndpoint, { method: "POST", headers: { "Content-Type": "application/json", @@ -397,13 +397,13 @@ export class PDPSubgraphService { } // Retry on network/HTTP errors - if (attempt < PDPSubgraphService.MAX_RETRIES) { - const delay = PDPSubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); + if (attempt < SubgraphService.MAX_RETRIES) { + const delay = SubgraphService.INITIAL_RETRY_DELAY_MS * (1 << (attempt - 1)); this.logger.warn({ event: "subgraph_provider_request_retry", message: "Subgraph provider request failed. Retrying...", attempt, - maxRetries: PDPSubgraphService.MAX_RETRIES, + maxRetries: SubgraphService.MAX_RETRIES, retryDelayMs: delay, addressCount: addresses.length, error: toStructuredError(error), @@ -415,13 +415,13 @@ export class PDPSubgraphService { this.logger.error({ event: "subgraph_provider_request_failed", message: "Subgraph provider request failed after maximum retries", - maxRetries: PDPSubgraphService.MAX_RETRIES, + maxRetries: SubgraphService.MAX_RETRIES, blockNumber, addressCount: addresses.length, error: toStructuredError(error), }); throw new Error( - `Failed to fetch provider data after ${PDPSubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, + `Failed to fetch provider data after ${SubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, ); } } @@ -432,18 +432,18 @@ export class PDPSubgraphService { * Read more here: https://docs.goldsky.com/subgraphs/graphql-endpoints#public-endpoints */ private async enforceRateLimit(requestCount: number = 1): Promise { - if (requestCount > PDPSubgraphService.MAX_CONCURRENT_REQUESTS) { + if (requestCount > SubgraphService.MAX_CONCURRENT_REQUESTS) { throw new Error( - `Cannot request ${requestCount} items; exceeds rate limit window of ${PDPSubgraphService.MAX_CONCURRENT_REQUESTS}`, + `Cannot request ${requestCount} items; exceeds rate limit window of ${SubgraphService.MAX_CONCURRENT_REQUESTS}`, ); } const now = Date.now(); - const windowStart = now - PDPSubgraphService.RATE_LIMIT_WINDOW_MS; + const windowStart = now - SubgraphService.RATE_LIMIT_WINDOW_MS; this.requestTimestamps = this.requestTimestamps.filter((timestamp) => timestamp > windowStart); - const availableSlots = PDPSubgraphService.MAX_CONCURRENT_REQUESTS - this.requestTimestamps.length; + const availableSlots = SubgraphService.MAX_CONCURRENT_REQUESTS - this.requestTimestamps.length; if (requestCount > availableSlots) { const requiredSlots = requestCount - availableSlots; @@ -452,7 +452,7 @@ export class PDPSubgraphService { const oldestTimestamp = this.requestTimestamps[index] || now; // wait time with 10ms buffer - const waitTime = oldestTimestamp + PDPSubgraphService.RATE_LIMIT_WINDOW_MS - now + 10; + const waitTime = oldestTimestamp + SubgraphService.RATE_LIMIT_WINDOW_MS - now + 10; if (waitTime > 0) { await new Promise((resolve) => setTimeout(resolve, waitTime)); diff --git a/apps/backend/src/pdp-subgraph/types.spec.ts b/apps/backend/src/subgraph/types.spec.ts similarity index 100% rename from apps/backend/src/pdp-subgraph/types.spec.ts rename to apps/backend/src/subgraph/types.spec.ts diff --git a/apps/backend/src/pdp-subgraph/types.ts b/apps/backend/src/subgraph/types.ts similarity index 100% rename from apps/backend/src/pdp-subgraph/types.ts rename to apps/backend/src/subgraph/types.ts diff --git a/apps/backend/src/wallet-sdk/wallet-sdk.service.spec.ts b/apps/backend/src/wallet-sdk/wallet-sdk.service.spec.ts index 9b0a7070..75078eee 100644 --- a/apps/backend/src/wallet-sdk/wallet-sdk.service.spec.ts +++ b/apps/backend/src/wallet-sdk/wallet-sdk.service.spec.ts @@ -17,7 +17,7 @@ const baseConfig: IBlockchainConfig = { checkDatasetCreationFees: false, useOnlyApprovedProviders: false, minNumDataSetsForChecks: 1, - pdpSubgraphEndpoint: "https://api.thegraph.com/subgraphs/filecoin/pdp", + subgraphEndpoint: "https://api.thegraph.com/subgraphs/filecoin/pdp", }; const makeProvider = (overrides: Partial): PDPProviderEx => diff --git a/apps/subgraph/README.md b/apps/subgraph/README.md index ff0532f6..2d893838 100644 --- a/apps/subgraph/README.md +++ b/apps/subgraph/README.md @@ -1,6 +1,6 @@ # @dealbot/subgraph -A dealbot-owned Graph Protocol subgraph indexing the Filecoin PDP contracts. Deployed to Goldsky and consumed exclusively by `apps/backend` via the `PDP_SUBGRAPH_ENDPOINT` env var. +A dealbot-owned Graph Protocol subgraph indexing the Filecoin PDP contracts. Deployed to Goldsky and consumed exclusively by `apps/backend` via the `SUBGRAPH_ENDPOINT` env var. ## What it indexes @@ -9,7 +9,7 @@ A dealbot-owned Graph Protocol subgraph indexing the Filecoin PDP contracts. Dep ## Why it exists -The dealbot backend needs three queries (see `apps/backend/src/pdp-subgraph/queries.ts`): +The dealbot backend needs three queries (see `apps/backend/src/subgraph/queries.ts`): 1. `GET_SUBGRAPH_META` — latest indexed block. 2. `GET_PROVIDERS_WITH_DATASETS` — overdue proving-period detection. @@ -73,4 +73,4 @@ Goldsky slots (slugs TBD): - `dealbot-mainnet/` — mainnet - `dealbot-calibration/` — calibration -After deploy, update `PDP_SUBGRAPH_ENDPOINT` in the backend env to the new `/gn` URL. +After deploy, update `SUBGRAPH_ENDPOINT` in the backend env to the new `/gn` URL. diff --git a/docs/checks/data-retention.md b/docs/checks/data-retention.md index 804190c6..e87fbb99 100644 --- a/docs/checks/data-retention.md +++ b/docs/checks/data-retention.md @@ -25,7 +25,7 @@ Dealbot polls The Graph API endpoint for PDP (Proof of Data Possession) data at **Subgraph repository**: [FilOzone/pdp-explorer](https://github.com/FilOzone/pdp-explorer/blob/main/subgraph/src/pdp-verifier.ts) -**Subgraph endpoint**: Configured via `PDP_SUBGRAPH_ENDPOINT` environment variable (see [environment-variables.md](../environment-variables.md#pdp_subgraph_endpoint)) +**Subgraph endpoint**: Configured via `SUBGRAPH_ENDPOINT` environment variable (see [environment-variables.md](../environment-variables.md#subgraph_endpoint)) > **Note**: The production subgraph URL is currently being finalized [here](https://github.com/FilOzone/pdp-explorer/pull/86). @@ -46,7 +46,7 @@ From `GET_PROVIDERS_WITH_DATASETS` query for each provider: > **Note**: The subgraph query uses the field name `proofSets`, but this refers to "dataSets" in the current codebase. The terminology was updated from "proof set" to "data set" but the subgraph schema retains the old naming. -Source: [`pdp-subgraph.service.ts` (`fetchSubgraphMeta`, `fetchProvidersWithDatasets`)](../../apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts) +Source: [`subgraph.service.ts` (`fetchSubgraphMeta`, `fetchProvidersWithDatasets`)](../../apps/backend/src/subgraph/subgraph.service.ts) ### 2. Compute Challenge Totals and Overdue Estimates @@ -164,7 +164,7 @@ The PDP subgraph service enforces Goldsky's public endpoint rate limits: Rate limiting is enforced client-side to prevent 429 errors. -Source: [`pdp-subgraph.service.ts` (`enforceRateLimit`)](../../apps/backend/src/pdp-subgraph/pdp-subgraph.service.ts) +Source: [`subgraph.service.ts` (`enforceRateLimit`)](../../apps/backend/src/subgraph/subgraph.service.ts) ## Metrics Recorded @@ -195,11 +195,11 @@ Key environment variables that control data retention check behavior: | Variable | Required | Default | Description | | ----------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------ | -| `PDP_SUBGRAPH_ENDPOINT` | No | Empty string | The Graph API endpoint for PDP subgraph queries. When empty, data retention checks are disabled. | +| `SUBGRAPH_ENDPOINT` | No | Empty string | The Graph API endpoint for PDP subgraph queries. When empty, data retention checks are disabled. | Source: [`app.config.ts`](../../apps/backend/src/config/app.config.ts) -See also: [`environment-variables.md`](../environment-variables.md#pdp_subgraph_endpoint) for the full configuration reference. +See also: [`environment-variables.md`](../environment-variables.md#subgraph_endpoint) for the full configuration reference. ## Error Handling diff --git a/docs/checks/production-configuration-and-approval-methodology.md b/docs/checks/production-configuration-and-approval-methodology.md index 6da7c92f..8c033f0d 100644 --- a/docs/checks/production-configuration-and-approval-methodology.md +++ b/docs/checks/production-configuration-and-approval-methodology.md @@ -40,7 +40,7 @@ Relevant parameters include: | Parameter | Value | Notes | |-----------|-------|-------| -| [`PDP_SUBGRAPH_ENDPOINT`](../environment-variables.md#pdp_subgraph_endpoint) | TODO: fill this in | Uses the subgraph from [pdp-explorer](https://github.com/FilOzone/pdp-explorer). | +| [`SUBGRAPH_ENDPOINT`](../environment-variables.md#subgraph_endpoint) | TODO: fill this in | Uses the subgraph from [pdp-explorer](https://github.com/FilOzone/pdp-explorer). | | [`MIN_NUM_DATASETS_FOR_CHECKS`](../environment-variables.md#dataset-configuration) | 15 | Ensure there are enough datasets with pieces being added so that statistical significance for [Data Retention Fault Rate](#data-retention-fault-rate) can be achieved quicker. Note that on mainnet each dataset incurs 5 challenges[^1] per daily proof[^2]. With this many datasets, an SP can be approved for data retention after a faultless ~7 days even if the SP doesn't have other datasets. | See [How are data retention statistics/thresholds calculated?](#how-are-data-retention-statisticsthresholds-calculated) for more details. diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 212b97a8..0a76cd83 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -8,7 +8,7 @@ This document provides a comprehensive guide to all environment variables used b | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Application](#application-configuration) | `NODE_ENV`, `DEALBOT_PORT`, `DEALBOT_HOST`, `DEALBOT_RUN_MODE`, `DEALBOT_METRICS_PORT`, `DEALBOT_METRICS_HOST`, `DEALBOT_ALLOWED_ORIGINS`, `ENABLE_DEV_MODE` | | [Database](#database-configuration) | `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_POOL_MAX`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_NAME` | -| [Blockchain](#blockchain-configuration) | `NETWORK`, `RPC_URL`, `WALLET_ADDRESS`, `WALLET_PRIVATE_KEY`, `SESSION_KEY_PRIVATE_KEY`, `CHECK_DATASET_CREATION_FEES`, `USE_ONLY_APPROVED_PROVIDERS`, `PDP_SUBGRAPH_ENDPOINT` | +| [Blockchain](#blockchain-configuration) | `NETWORK`, `RPC_URL`, `WALLET_ADDRESS`, `WALLET_PRIVATE_KEY`, `SESSION_KEY_PRIVATE_KEY`, `CHECK_DATASET_CREATION_FEES`, `USE_ONLY_APPROVED_PROVIDERS`, `SUBGRAPH_ENDPOINT` | | [Dataset Versioning](#dataset-versioning) | `DEALBOT_DATASET_VERSION` | | [Scheduling](#scheduling-configuration) | `PROVIDERS_REFRESH_INTERVAL_SECONDS`, `DATA_RETENTION_POLL_INTERVAL_SECONDS`, `DEALBOT_MAINTENANCE_WINDOWS_UTC`, `DEALBOT_MAINTENANCE_WINDOW_MINUTES` | | [Jobs (pg-boss)](#jobs-pg-boss) | `DEALBOT_PGBOSS_SCHEDULER_ENABLED`, `DEALBOT_PGBOSS_POOL_MAX`, `DEALS_PER_SP_PER_HOUR`, `DATASET_CREATIONS_PER_SP_PER_HOUR`, `RETRIEVALS_PER_SP_PER_HOUR`, `JOB_SCHEDULER_POLL_SECONDS`, `JOB_WORKER_POLL_SECONDS`, `PG_BOSS_LOCAL_CONCURRENCY`, `JOB_CATCHUP_MAX_ENQUEUE`, `JOB_SCHEDULE_PHASE_SECONDS`, `JOB_ENQUEUE_JITTER_SECONDS`, `DEAL_JOB_TIMEOUT_SECONDS`, `RETRIEVAL_JOB_TIMEOUT_SECONDS`, `IPFS_BLOCK_FETCH_CONCURRENCY` | @@ -424,7 +424,7 @@ Session keys are scoped (only storage operations, not deposits or withdrawals) a --- -### `PDP_SUBGRAPH_ENDPOINT` +### `SUBGRAPH_ENDPOINT` - **Type**: `string` (URL) - **Required**: No @@ -442,7 +442,7 @@ The dealbot-owned subgraph lives at `apps/subgraph/` (package `@dealbot/subgraph **Example**: ```bash -PDP_SUBGRAPH_ENDPOINT=https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn +SUBGRAPH_ENDPOINT=https://api.goldsky.com/api/public//subgraphs/dealbot-subgraph//gn ``` --- diff --git a/kustomize/overlays/local/backend-configmap-local.yaml b/kustomize/overlays/local/backend-configmap-local.yaml index 704d444f..937b9ed6 100644 --- a/kustomize/overlays/local/backend-configmap-local.yaml +++ b/kustomize/overlays/local/backend-configmap-local.yaml @@ -26,5 +26,5 @@ data: PG_BOSS_LOCAL_CONCURRENCY: "3" JOB_WORKER_POLL_SECONDS: "60" RANDOM_PIECE_SIZES: "10485760" - PDP_SUBGRAPH_ENDPOINT: "https://api.goldsky.com/api/public/project_cmdfaaxeuz6us01u359yjdctw/subgraphs/pdp-explorer/calibration311a/gn" + SUBGRAPH_ENDPOINT: "https://api.goldsky.com/api/public/project_cmdfaaxeuz6us01u359yjdctw/subgraphs/pdp-explorer/calibration311a/gn" JOB_SCHEDULER_POLL_SECONDS: "60" From 55b9187733465d4f6e0126e088306df3533aa3e6 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 15:07:11 +0200 Subject: [PATCH 09/19] refactor(retrieval-anon): random piece selection --- apps/backend/src/retrieval-anon/README.md | 155 +++++++++++++++ .../anon-piece-selector.service.spec.ts | 140 ++++++++----- .../anon-piece-selector.service.ts | 185 ++++++++++++++---- apps/backend/src/subgraph/queries.ts | 95 ++++++--- .../src/subgraph/subgraph.service.spec.ts | 158 +++++++-------- apps/backend/src/subgraph/subgraph.service.ts | 128 ++++++------ apps/backend/src/subgraph/types.ts | 67 ++++--- apps/subgraph/schema.graphql | 6 + apps/subgraph/src/helpers.ts | 9 +- apps/subgraph/src/pdp-verifier.ts | 2 + apps/subgraph/subgraph.yaml | 12 +- apps/subgraph/tests/pdp-verifier.test.ts | 6 +- 12 files changed, 672 insertions(+), 291 deletions(-) create mode 100644 apps/backend/src/retrieval-anon/README.md diff --git a/apps/backend/src/retrieval-anon/README.md b/apps/backend/src/retrieval-anon/README.md new file mode 100644 index 00000000..7abf9f8c --- /dev/null +++ b/apps/backend/src/retrieval-anon/README.md @@ -0,0 +1,155 @@ +# Anonymous retrieval check + +The `retrievalAnon` check probes an SP using pieces the dealbot did **not** +upload itself, to detect SPs that serve the dealbot's own deals well but +perform poorly on arbitrary storage. See +[issue #427](https://github.com/FilOzone/dealbot/issues/427) for the full +motivation. + +This document describes the **piece selection** step. The subsequent piece +retrieval, CommP verification, CAR validation, IPNI lookup and `/ipfs` block +fetching follow the same shape as the basic retrieval check. + +## Goals + +1. **Uniform random** across the SP's entire active pool — not biased toward + recent writes, specific payers, or specific sizes. +2. **Prefer `withIPFSIndexing` pieces** so CAR/IPNI validation has something + meaningful to check, but still exercise non-indexed pieces so an SP can't + optimise only its CAR corpus. +3. **Cover a realistic spread of piece sizes** — big enough for useful + bandwidth measurements, not so big that SPs with only small deals are + skipped. +4. **Respect termination signals** — exclude datasets with `isActive: false`, + `fwssPayer == dealbot`, or `pdpPaymentEndEpoch <= currentEpoch`. +5. **Avoid immediate repeats** — don't retest a piece already tested in the + last 500 anonymous retrievals. + +## How it works + +Every `Root` in the subgraph carries a `sampleKey = keccak256(id)` populated +once when the root is indexed. Because keccak256 is uniform over 256 bits and +independent of creation order, dataset, and size, `sampleKey` sorts roots +into a uniform random permutation that is stable across queries. + +To draw a sample the backend: + +1. Picks a **size bucket** by weighted random: + + | bucket | size range (raw bytes) | weight | + |---|---|---| + | `small` | `[1, 64 MiB)` | 0.2 | + | `medium` | `[64 MiB, 1 GiB)` | 0.5 | + | `large` | `[1 GiB, 32 GiB]` | 0.3 | + +2. Picks the **pool**: `withIPFSIndexing: true` with probability 0.8; + otherwise no filter on `withIPFSIndexing` (both indexed and non-indexed + pieces are eligible). + +3. Generates 32 random bytes as `$sampleKey` and queries: + + ```graphql + roots( + first: 1 + orderBy: sampleKey + orderDirection: asc + where: { + sampleKey_gte: $sampleKey + removed: false + rawSize_gte: $minSize + rawSize_lte: $maxSize + proofSet_: { + fwssServiceProvider: $sp + fwssPayer_not: $dealbotPayer + isActive: true + withIPFSIndexing: true # only for the "indexed" pool query + } + } + ) { rootId cid rawSize ipfsRootCID proofSet { setId withIPFSIndexing fwssPayer pdpPaymentEndEpoch } } + ``` + + The result is the root with the smallest `sampleKey ≥ $sampleKey` that + satisfies the filters — a uniform random pick, in O(log N), with no + `skip` ceiling. + +4. Drops the pick if `pdpPaymentEndEpoch` has already passed the latest + indexed block, or if its CID appears in the last 500 anonymous + retrievals. On a drop, redraws once with a fresh `$sampleKey`. + +5. If no piece survives, falls back through this order: + + 1. Same bucket, opposite pool. + 2. Any bucket (`[0, 2^63-1]`), indexed pool. + 3. Any bucket, any pool. + + Each attempt uses a fresh `$sampleKey` and does up to two draws before + moving on. + +### Worked example + +An SP with 50k active FWSS pieces is up for a probe. + +1. Weighted random picks `medium`. Coin flip picks `indexed` pool. +2. `$sampleKey = 0x7fb3…c91e`. Subgraph returns the piece with the smallest + `sampleKey ≥ $sampleKey` whose raw size is in `[64 MiB, 1 GiB)`, whose + dataset is active, not paid by the dealbot, and marked + `withIPFSIndexing`. +3. Its `pdpPaymentEndEpoch` is null and its CID isn't in the last 500 anon + retrievals. Accepted. + +Total: one subgraph call. For an SP whose `medium/indexed` pool is empty +(small, non-CAR-heavy SP) the selector redraws once, tries `medium/any` +twice, and will land in `any/indexed` or `any/any` within a couple hundred +milliseconds. + +## Why a dedicated `sampleKey` field? + +GraphQL has no native random operator. The two obvious alternatives both +break at the scales FOC expects: + +- **`first: 1, skip: random(count)`** — The Graph caps `skip` at 5000. A + single mainnet SP can already exceed this. +- **Ordering by `id`** — `Root.id = "-"` is clustered by + dataset age (and lexicographically quirky: `"1-10" < "1-2"`), so random + `id_gte` picks skew heavily toward whichever `setId` prefix the random + hex lands on. + +A precomputed keccak hash sidesteps both: the sort is uniform, the lookup is +indexed (graph-node indexes every scalar automatically), and the field is +immutable — no maintenance cost on Root updates. + +## What this replaces + +The previous selector fetched the last 100 datasets × last 50 roots for an +SP, filtered out dealbot-owned and terminated ones client-side, and picked +from an in-memory pool with an "prefer IPFS-indexed" post-filter. That +implementation had three defects this design fixes: + +1. An SP with more than 100 datasets, or more than 50 roots per dataset, + was only ever probed on its newest corner of storage. +2. "Prefer IPFS-indexed" was applied after the pool was already truncated + to the 100×50 recent window — indexed pieces outside that window were + unreachable. +3. A cross-SP 500-piece dedup window was applied to a small per-SP pool, so + it could starve out quickly for busy SPs. + +The new selector also: + +- Enforces size bucketing (`small / medium / large`), addressing rvagg's + "big enough for bandwidth metrics, not so big as to exclude small SPs" + concern. +- Moves `isActive` and `fwssPayer_not: dealbot` into the subgraph `where:` + clause (one query round-trip instead of a client-side filter loop). +- Keeps `pdpPaymentEndEpoch` as a client-side check because GraphQL + nullable-BigInt comparison semantics would require multiple queries. + +## Tunables + +All in `anon-piece-selector.service.ts`: + +- `SIZE_BUCKETS` — bucket boundaries. +- `BUCKET_WEIGHTS` — bucket draw probabilities (must sum to 1). +- `IPFS_INDEXED_SAMPLE_RATE` — fraction of draws that start in the indexed + pool (default 0.8). +- `RECENT_DEDUP_WINDOW` — how many recent anon retrievals are excluded + (default 500). diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts index cf3a1cea..78a4e5fd 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts @@ -3,20 +3,22 @@ import type { Repository } from "typeorm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { IConfig } from "../config/app.config.js"; import type { Retrieval } from "../database/entities/retrieval.entity.js"; -import type { SubgraphService } from "../subgraph/subgraph.service.js"; -import type { FwssCandidatePiece } from "../subgraph/types.js"; +import type { SampleAnonPieceParams, SubgraphService } from "../subgraph/subgraph.service.js"; +import type { AnonCandidatePiece } from "../subgraph/types.js"; import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; const SP_ADDRESS = "0xAaAaAAaAaaaAaAAAAaaaaAAaaAaaaAAaaaaa1111"; const DEALBOT_PAYER = "0xBbBBBbBBbbbBbBBBBBbbbbbBBbbBbbbBBbbbb2222"; -const makePiece = (overrides: Partial = {}): FwssCandidatePiece => ({ +const makePiece = (overrides: Partial = {}): AnonCandidatePiece => ({ pieceCid: `baga6ea4seaqpiece${Math.random().toString(36).slice(2, 10)}`, pieceId: "1", dataSetId: "42", rawSize: "1048576", withIPFSIndexing: true, ipfsRootCid: "bafyroot", + indexedAtBlock: 12345, + pdpPaymentEndEpoch: null, ...overrides, }); @@ -46,91 +48,123 @@ const makeConfigService = (): ConfigService => describe("AnonPieceSelectorService", () => { let subgraphService: SubgraphService; - let listFwssCandidatePieces: ReturnType; + let sampleAnonPiece: ReturnType; beforeEach(() => { - listFwssCandidatePieces = vi.fn(); - subgraphService = { listFwssCandidatePieces } as unknown as SubgraphService; + sampleAnonPiece = vi.fn(); + subgraphService = { sampleAnonPiece } as unknown as SubgraphService; }); - it("returns null when the subgraph yields no candidates", async () => { - listFwssCandidatePieces.mockResolvedValue([]); + it("returns null when every fallback attempt yields no piece", async () => { + sampleAnonPiece.mockResolvedValue(null); const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); const result = await service.selectPieceForProvider(SP_ADDRESS); expect(result).toBeNull(); - expect(listFwssCandidatePieces).toHaveBeenCalledWith(SP_ADDRESS, DEALBOT_PAYER); + expect(sampleAnonPiece).toHaveBeenCalled(); }); - it("filters out pieces tested in the recent retrieval window", async () => { - const freshCid = "baga6ea4seaqfresh"; - const staleCid = "baga6ea4seaqstale"; - listFwssCandidatePieces.mockResolvedValue([ - makePiece({ pieceCid: staleCid, pieceId: "1" }), - makePiece({ pieceCid: freshCid, pieceId: "2" }), - ]); - const service = new AnonPieceSelectorService( - subgraphService, - makeConfigService(), - makeRetrievalRepository([staleCid]), - ); + it("returns the sampled piece with SP address lowercased", async () => { + sampleAnonPiece.mockResolvedValueOnce(makePiece({ pieceCid: "baga-the-one" })); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); const result = await service.selectPieceForProvider(SP_ADDRESS); expect(result).not.toBeNull(); - expect(result?.pieceCid).toBe(freshCid); + expect(result?.pieceCid).toBe("baga-the-one"); + expect(result?.serviceProvider).toBe(SP_ADDRESS.toLowerCase()); }); - it("falls back to the full candidate pool when every piece has been tested recently", async () => { - const cid = "baga6ea4seaqonly"; - listFwssCandidatePieces.mockResolvedValue([makePiece({ pieceCid: cid })]); - const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([cid])); + it("passes the dealbot payer address to sampleAnonPiece for exclusion", async () => { + sampleAnonPiece.mockResolvedValueOnce(makePiece()); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); - const result = await service.selectPieceForProvider(SP_ADDRESS); + await service.selectPieceForProvider(SP_ADDRESS); - expect(result?.pieceCid).toBe(cid); + const call = sampleAnonPiece.mock.calls[0][0] as SampleAnonPieceParams; + expect(call.payer).toBe(DEALBOT_PAYER); + expect(call.serviceProvider).toBe(SP_ADDRESS); }); - it("prefers IPFS-indexed pieces with an ipfsRootCid when selecting", async () => { - const pieces = [ - makePiece({ pieceCid: "baga-plain-1", withIPFSIndexing: false, ipfsRootCid: null }), - makePiece({ pieceCid: "baga-indexed-1", withIPFSIndexing: true, ipfsRootCid: "bafy1" }), - makePiece({ pieceCid: "baga-plain-2", withIPFSIndexing: false, ipfsRootCid: null }), - makePiece({ pieceCid: "baga-indexed-2", withIPFSIndexing: true, ipfsRootCid: "bafy2" }), - ]; - listFwssCandidatePieces.mockResolvedValue(pieces); + it("redraws when the first sampled piece's payment has already terminated", async () => { + const staleCid = "baga-terminated"; + const freshCid = "baga-live"; + sampleAnonPiece + .mockResolvedValueOnce(makePiece({ pieceCid: staleCid, pdpPaymentEndEpoch: 100n, indexedAtBlock: 200 })) + .mockResolvedValueOnce(makePiece({ pieceCid: freshCid, pdpPaymentEndEpoch: null })); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + const result = await service.selectPieceForProvider(SP_ADDRESS); + + expect(result?.pieceCid).toBe(freshCid); + }); - const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0); + it("redraws when the first sampled piece was recently tested", async () => { + const staleCid = "baga-stale"; + const freshCid = "baga-fresh"; + sampleAnonPiece + .mockResolvedValueOnce(makePiece({ pieceCid: staleCid })) + .mockResolvedValueOnce(makePiece({ pieceCid: freshCid })); + const service = new AnonPieceSelectorService( + subgraphService, + makeConfigService(), + makeRetrievalRepository([staleCid]), + ); const result = await service.selectPieceForProvider(SP_ADDRESS); - expect(result?.pieceCid).toBe("baga-indexed-1"); - randomSpy.mockRestore(); + expect(result?.pieceCid).toBe(freshCid); }); - it("falls back to all pieces when none are IPFS-indexed", async () => { - const pieces = [ - makePiece({ pieceCid: "baga-plain-1", withIPFSIndexing: false, ipfsRootCid: null }), - makePiece({ pieceCid: "baga-plain-2", withIPFSIndexing: true, ipfsRootCid: null }), - ]; - listFwssCandidatePieces.mockResolvedValue(pieces); - const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + it("falls back to the opposite pool when the preferred one is empty", async () => { + // First pool call returns nothing twice (both attempts), second pool succeeds. + const fresh = makePiece({ pieceCid: "baga-other-pool" }); + sampleAnonPiece.mockResolvedValueOnce(null).mockResolvedValueOnce(null).mockResolvedValueOnce(fresh); - const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); const result = await service.selectPieceForProvider(SP_ADDRESS); - expect(["baga-plain-1", "baga-plain-2"]).toContain(result?.pieceCid); - randomSpy.mockRestore(); + expect(result?.pieceCid).toBe("baga-other-pool"); + + // The second (fallback) call should target the opposite pool. + const firstCall = sampleAnonPiece.mock.calls[0][0] as SampleAnonPieceParams; + const fallbackCall = sampleAnonPiece.mock.calls[2][0] as SampleAnonPieceParams; + expect(fallbackCall.pool).not.toBe(firstCall.pool); }); - it("returns lowercase SP address on the selected piece", async () => { - listFwssCandidatePieces.mockResolvedValue([makePiece()]); - const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + it("widens size bucket to 'any' after both pools fail in the primary bucket", async () => { + // 4 empty attempts across (bucket × both pools × 2 draws each) then + // succeed on the first `any` bucket call. + sampleAnonPiece + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(makePiece({ pieceCid: "baga-any-bucket" })); + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); const result = await service.selectPieceForProvider(SP_ADDRESS); - expect(result?.serviceProvider).toBe(SP_ADDRESS.toLowerCase()); + expect(result?.pieceCid).toBe("baga-any-bucket"); + + // The 5th call (index 4) should be the widened-bucket attempt; its size + // range covers at least the 32 GiB ceiling of the "large" bucket. + const widened = sampleAnonPiece.mock.calls[4][0] as SampleAnonPieceParams; + expect(BigInt(widened.maxSize)).toBeGreaterThanOrEqual(32n * 1024n * 1024n * 1024n); + expect(widened.minSize).toBe("0"); + }); + + it("draws a fresh sampleKey for each subgraph call", async () => { + sampleAnonPiece.mockResolvedValueOnce(null).mockResolvedValueOnce(makePiece()); + + const service = new AnonPieceSelectorService(subgraphService, makeConfigService(), makeRetrievalRepository([])); + await service.selectPieceForProvider(SP_ADDRESS); + + const call1 = sampleAnonPiece.mock.calls[0][0] as SampleAnonPieceParams; + const call2 = sampleAnonPiece.mock.calls[1][0] as SampleAnonPieceParams; + expect(call1.sampleKey).toMatch(/^0x[0-9a-f]{64}$/); + expect(call2.sampleKey).toMatch(/^0x[0-9a-f]{64}$/); + expect(call1.sampleKey).not.toBe(call2.sampleKey); }); }); diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts index 8e4eadbd..6744bfc3 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts @@ -1,11 +1,13 @@ +import { randomBytes } from "node:crypto"; import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { InjectRepository } from "@nestjs/typeorm"; import type { Repository } from "typeorm"; import type { IConfig } from "../config/app.config.js"; import { Retrieval } from "../database/entities/retrieval.entity.js"; +import type { AnonPiecePool, SampleAnonPieceParams } from "../subgraph/subgraph.service.js"; import { SubgraphService } from "../subgraph/subgraph.service.js"; -import type { FwssCandidatePiece } from "../subgraph/types.js"; +import type { AnonCandidatePiece } from "../subgraph/types.js"; import type { AnonPiece } from "./types.js"; /** @@ -16,6 +18,37 @@ import type { AnonPiece } from "./types.js"; */ const RECENT_DEDUP_WINDOW = 500; +/** + * Piece size buckets, in raw (unpadded) bytes. Weighted sampling across + * these buckets keeps tests meaningful for bandwidth measurement without + * locking out SPs whose corpus skews small or large. + */ +type SizeBucket = "small" | "medium" | "large"; +type SizeRange = { min: bigint; max: bigint }; + +const MIB = 1024n * 1024n; +const GIB = 1024n * MIB; + +const SIZE_BUCKETS: Record = { + small: { min: 1n, max: 64n * MIB - 1n }, + medium: { min: 64n * MIB, max: 1n * GIB - 1n }, + large: { min: 1n * GIB, max: 32n * GIB }, +}; + +/** Weights for choosing a bucket per selection. Must sum to 1. */ +const BUCKET_WEIGHTS: Record = { + small: 0.2, + medium: 0.5, + large: 0.3, +}; + +/** + * Probability the primary draw targets the withIPFSIndexing pool. + * The rest of the time we sample across all FWSS pieces so SPs can't + * optimise only their CAR corpus. + */ +const IPFS_INDEXED_SAMPLE_RATE = 0.8; + @Injectable() export class AnonPieceSelectorService { private readonly logger = new Logger(AnonPieceSelectorService.name); @@ -30,49 +63,121 @@ export class AnonPieceSelectorService { /** * Select an anonymous piece to test against the given SP. * - * Queries the FWSS subgraph for candidate pieces, filters out pieces - * tested in the last RECENT_DEDUP_WINDOW anonymous retrievals, and - * picks one uniformly at random — preferring pieces with a declared - * ipfsRootCID so CAR/IPNI validation has something meaningful to check. + * Strategy (see README.md for the full rationale): + * 1. Pick a size bucket by weighted random. + * 2. Pick a pool (`indexed` 80% / `any` 20%). + * 3. Generate a uniform-random sampleKey and query the subgraph for the + * smallest `Root.sampleKey ≥ $sampleKey` matching the filters. + * 4. Drop the pick if `pdpPaymentEndEpoch` has passed or it was tested + * recently; redraw once. + * 5. If still empty, fall back through: (same bucket, opposite pool) → + * (any bucket, indexed) → (any bucket, any). */ async selectPieceForProvider(spAddress: string): Promise { const dealbotPayer = this.configService.get("blockchain", { infer: true }).walletAddress; - const candidates = await this.subgraphService.listFwssCandidatePieces(spAddress, dealbotPayer); + const recentlyTested = await this.loadRecentlyTestedPieceCids(); + + const bucket = this.pickBucket(); + const pool: AnonPiecePool = Math.random() < IPFS_INDEXED_SAMPLE_RATE ? "indexed" : "any"; - if (candidates.length === 0) { - this.logger.warn({ - event: "anon_no_candidates", - message: "FWSS subgraph returned no candidate pieces for SP", + const attempts: Array<{ bucket: SizeBucket | "any"; pool: AnonPiecePool }> = [ + { bucket, pool }, + { bucket, pool: pool === "indexed" ? "any" : "indexed" }, + { bucket: "any", pool: "indexed" }, + { bucket: "any", pool: "any" }, + ]; + + for (const attempt of attempts) { + const piece = await this.drawPiece({ spAddress, + dealbotPayer, + bucket: attempt.bucket, + pool: attempt.pool, + recentlyTested, }); - return null; - } - const recentlyTested = await this.loadRecentlyTestedPieceCids(); - const fresh = candidates.filter((c) => !recentlyTested.has(c.pieceCid)); - const pool = fresh.length > 0 ? fresh : candidates; - - const picked = this.pickPreferringIpfsIndexed(pool); + if (piece) { + this.logger.log({ + event: "anon_piece_selected", + message: "Selected anonymous piece for retrieval test", + spAddress, + pieceCid: piece.pieceCid, + dataSetId: piece.dataSetId, + withIPFSIndexing: piece.withIPFSIndexing, + bucket: attempt.bucket, + pool: attempt.pool, + }); + return { + pieceCid: piece.pieceCid, + dataSetId: piece.dataSetId, + pieceId: piece.pieceId, + serviceProvider: spAddress.toLowerCase(), + withIPFSIndexing: piece.withIPFSIndexing, + ipfsRootCid: piece.ipfsRootCid, + }; + } + } - this.logger.log({ - event: "anon_piece_selected", - message: "Selected anonymous piece for retrieval test", + this.logger.warn({ + event: "anon_no_candidates", + message: "No anonymous piece found after all fallbacks", spAddress, - pieceCid: picked.pieceCid, - dataSetId: picked.dataSetId, - withIPFSIndexing: picked.withIPFSIndexing, - candidateCount: candidates.length, - freshCount: fresh.length, }); + return null; + } - return { - pieceCid: picked.pieceCid, - dataSetId: picked.dataSetId, - pieceId: picked.pieceId, - serviceProvider: spAddress.toLowerCase(), - withIPFSIndexing: picked.withIPFSIndexing, - ipfsRootCid: picked.ipfsRootCid, - }; + /** + * Try to draw a piece for one (bucket, pool) combination. Up to two draws + * with fresh sampleKeys, each filtered by dedup + epoch-termination. + */ + private async drawPiece(args: { + spAddress: string; + dealbotPayer: string; + bucket: SizeBucket | "any"; + pool: AnonPiecePool; + recentlyTested: Set; + }): Promise { + const range = args.bucket === "any" ? fullRange() : SIZE_BUCKETS[args.bucket]; + + for (let attempt = 0; attempt < 2; attempt++) { + const params: SampleAnonPieceParams = { + serviceProvider: args.spAddress, + payer: args.dealbotPayer, + sampleKey: randomSampleKey(), + minSize: range.min.toString(), + maxSize: range.max.toString(), + pool: args.pool, + }; + + const piece = await this.subgraphService.sampleAnonPiece(params); + if (!piece) { + continue; + } + + if (piece.pdpPaymentEndEpoch != null && piece.pdpPaymentEndEpoch <= BigInt(piece.indexedAtBlock)) { + continue; + } + + if (args.recentlyTested.has(piece.pieceCid)) { + continue; + } + + return piece; + } + + return null; + } + + private pickBucket(): SizeBucket { + const r = Math.random(); + let acc = 0; + for (const [name, weight] of Object.entries(BUCKET_WEIGHTS) as Array<[SizeBucket, number]>) { + acc += weight; + if (r < acc) { + return name; + } + } + return "medium"; } /** @@ -91,10 +196,14 @@ export class AnonPieceSelectorService { return new Set(rows.map((row) => row.anonPieceCid)); } +} - private pickPreferringIpfsIndexed(pool: FwssCandidatePiece[]): FwssCandidatePiece { - const ipfsIndexed = pool.filter((p) => p.withIPFSIndexing && p.ipfsRootCid); - const effective = ipfsIndexed.length > 0 ? ipfsIndexed : pool; - return effective[Math.floor(Math.random() * effective.length)]; - } +/** Uniform-random 32-byte sort key as `0x`-prefixed hex. */ +function randomSampleKey(): string { + return `0x${randomBytes(32).toString("hex")}`; +} + +/** The full size range (used when bucket fallback is "any"). */ +function fullRange(): SizeRange { + return { min: 0n, max: (1n << 63n) - 1n }; } diff --git a/apps/backend/src/subgraph/queries.ts b/apps/backend/src/subgraph/queries.ts index 3f750da4..fe42eef6 100644 --- a/apps/backend/src/subgraph/queries.ts +++ b/apps/backend/src/subgraph/queries.ts @@ -21,42 +21,89 @@ export const Queries = { } } `, - GET_FWSS_CANDIDATE_PIECES: ` - query GetFwssCandidatePieces( + SAMPLE_ANON_PIECE_INDEXED: ` + query SampleAnonPieceIndexed( $serviceProvider: Bytes! $payer: Bytes! - $datasetLimit: Int! - $pieceLimit: Int! + $sampleKey: Bytes! + $minSize: BigInt! + $maxSize: BigInt! ) { _meta { block { number } } - dataSets( + roots( + first: 1 + orderBy: sampleKey + orderDirection: asc where: { - fwssServiceProvider: $serviceProvider - fwssPayer_not: $payer - isActive: true + sampleKey_gte: $sampleKey + removed: false + rawSize_gte: $minSize + rawSize_lte: $maxSize + proofSet_: { + fwssServiceProvider: $serviceProvider + fwssPayer_not: $payer + isActive: true + withIPFSIndexing: true + } + } + subgraphError: allow + ) { + rootId + cid + rawSize + ipfsRootCID + proofSet { + setId + withIPFSIndexing + fwssPayer + pdpPaymentEndEpoch + } + } + } + `, + SAMPLE_ANON_PIECE_ANY: ` + query SampleAnonPieceAny( + $serviceProvider: Bytes! + $payer: Bytes! + $sampleKey: Bytes! + $minSize: BigInt! + $maxSize: BigInt! + ) { + _meta { + block { + number + } + } + roots( + first: 1 + orderBy: sampleKey + orderDirection: asc + where: { + sampleKey_gte: $sampleKey + removed: false + rawSize_gte: $minSize + rawSize_lte: $maxSize + proofSet_: { + fwssServiceProvider: $serviceProvider + fwssPayer_not: $payer + isActive: true + } } - first: $datasetLimit - orderBy: createdAt - orderDirection: desc subgraphError: allow ) { - setId - withIPFSIndexing - pdpPaymentEndEpoch - roots( - where: { removed: false } - first: $pieceLimit - orderBy: createdAt - orderDirection: desc - ) { - rootId - cid - rawSize - ipfsRootCID + rootId + cid + rawSize + ipfsRootCID + proofSet { + setId + withIPFSIndexing + fwssPayer + pdpPaymentEndEpoch } } } diff --git a/apps/backend/src/subgraph/subgraph.service.spec.ts b/apps/backend/src/subgraph/subgraph.service.spec.ts index 529b1ba3..4dc2cd5e 100644 --- a/apps/backend/src/subgraph/subgraph.service.spec.ts +++ b/apps/backend/src/subgraph/subgraph.service.spec.ts @@ -41,27 +41,36 @@ const FWSS_PAYER = "0xBBbbBBbbBBbBBbBbbBBbbBBbbbbBbBBbbBBbb222"; const EXAMPLE_PIECE_CID = "baga6ea4seaqpzwrimvoc4jp4l7mk6knsknf6owsc2ev4krrs2peenl5qelh6u4y"; const pieceCidHex = `0x${Buffer.from(CID.parse(EXAMPLE_PIECE_CID).bytes).toString("hex")}`; -const makeCandidateResponse = (dataSets: Record[] = [], blockNumber = 12345) => ({ +const makeSampleRoot = (overrides: Record = {}) => ({ + rootId: "1", + cid: pieceCidHex, + rawSize: "1048576", + ipfsRootCID: "bafyroot", + proofSet: { + setId: "42", + withIPFSIndexing: true, + fwssPayer: FWSS_PAYER.toLowerCase(), + pdpPaymentEndEpoch: null, + }, + ...overrides, +}); + +const makeSampleResponse = (roots: Record[] = [], blockNumber = 12345) => ({ data: { _meta: { block: { number: blockNumber } }, - dataSets, + roots, }, }); -const makeFwssDataSet = (overrides: Record = {}) => ({ - setId: "42", - withIPFSIndexing: true, - pdpPaymentEndEpoch: null, - roots: [ - { - rootId: "1", - cid: pieceCidHex, - rawSize: "1048576", - ipfsRootCID: "bafyroot", - }, - ], - ...overrides, -}); +const SAMPLE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001"; +const defaultSampleParams = { + serviceProvider: FWSS_SP_ADDRESS, + payer: FWSS_PAYER, + sampleKey: SAMPLE_KEY, + minSize: "0", + maxSize: "1000000000000", + pool: "indexed" as const, +}; describe("SubgraphService", () => { let service: SubgraphService; @@ -720,125 +729,118 @@ describe("SubgraphService", () => { }); }); - describe("listFwssCandidatePieces", () => { - it("returns empty array when endpoint is not configured", async () => { + describe("sampleAnonPiece", () => { + it("returns null when endpoint is not configured", async () => { const noEndpointConfig = { get: vi.fn(() => ({ subgraphEndpoint: "" })), } as unknown as ConfigService; const noEndpointService = new SubgraphService(noEndpointConfig); - const pieces = await noEndpointService.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); - expect(pieces).toEqual([]); + const piece = await noEndpointService.sampleAnonPiece(defaultSampleParams); + expect(piece).toBeNull(); expect(fetchMock).not.toHaveBeenCalled(); }); - it("parses datasets and returns decoded candidate pieces", async () => { + it("returns null when the subgraph yields no matching root", async () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: async () => makeCandidateResponse([makeFwssDataSet()]), + json: async () => makeSampleResponse([]), }); - const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + const piece = await service.sampleAnonPiece(defaultSampleParams); + expect(piece).toBeNull(); + }); - expect(pieces).toHaveLength(1); - expect(pieces[0]).toMatchObject({ + it("parses the sampled root into a decoded candidate piece", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => makeSampleResponse([makeSampleRoot()]), + }); + + const piece = await service.sampleAnonPiece(defaultSampleParams); + + expect(piece).toMatchObject({ pieceCid: EXAMPLE_PIECE_CID, pieceId: "1", dataSetId: "42", rawSize: "1048576", withIPFSIndexing: true, ipfsRootCid: "bafyroot", + pdpPaymentEndEpoch: null, + indexedAtBlock: 12345, }); }); - it("lowercases SP and payer addresses before querying", async () => { + it("returns pdpPaymentEndEpoch as bigint when the dataset is terminating", async () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: async () => makeCandidateResponse([]), + json: async () => + makeSampleResponse([ + makeSampleRoot({ + proofSet: { + setId: "42", + withIPFSIndexing: true, + fwssPayer: FWSS_PAYER.toLowerCase(), + pdpPaymentEndEpoch: "5000", + }, + }), + ]), }); - await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + const piece = await service.sampleAnonPiece(defaultSampleParams); + expect(piece?.pdpPaymentEndEpoch).toBe(5000n); + }); + + it("lowercases SP and payer addresses before querying", async () => { + fetchMock.mockResolvedValueOnce({ ok: true, json: async () => makeSampleResponse([]) }); + + await service.sampleAnonPiece(defaultSampleParams); const [, opts] = fetchMock.mock.calls[0]; const body = JSON.parse(opts.body as string); expect(body.variables.serviceProvider).toBe(FWSS_SP_ADDRESS.toLowerCase()); expect(body.variables.payer).toBe(FWSS_PAYER.toLowerCase()); + expect(body.query).toContain("withIPFSIndexing: true"); }); - it("filters out datasets whose pdpPaymentEndEpoch has already passed", async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, - json: async () => - makeCandidateResponse( - [ - makeFwssDataSet({ setId: "10", pdpPaymentEndEpoch: "5000" }), - makeFwssDataSet({ setId: "11", pdpPaymentEndEpoch: "20000" }), - makeFwssDataSet({ setId: "12", pdpPaymentEndEpoch: null }), - ], - 10_000, - ), - }); - - const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + it("uses the any-pool query when pool is 'any'", async () => { + fetchMock.mockResolvedValueOnce({ ok: true, json: async () => makeSampleResponse([]) }); - const dataSetIds = pieces.map((p) => p.dataSetId).sort(); - expect(dataSetIds).toEqual(["11", "12"]); - }); + await service.sampleAnonPiece({ ...defaultSampleParams, pool: "any" }); - it("skips pieces whose CID fails to decode but keeps valid ones", async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, - json: async () => - makeCandidateResponse([ - makeFwssDataSet({ - roots: [ - { rootId: "1", cid: pieceCidHex, rawSize: "1024", ipfsRootCID: null }, - { rootId: "2", cid: "0xdeadbeef", rawSize: "2048", ipfsRootCID: null }, - ], - }), - ]), - }); - - const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); - expect(pieces).toHaveLength(1); - expect(pieces[0].pieceId).toBe("1"); + const [, opts] = fetchMock.mock.calls[0]; + const body = JSON.parse(opts.body as string); + expect(body.query).not.toContain("withIPFSIndexing: true"); }); - it("propagates null ipfsRootCID through to the candidate piece", async () => { + it("returns null when the sampled root has an undecodable CID", async () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: async () => - makeCandidateResponse([ - makeFwssDataSet({ - withIPFSIndexing: false, - roots: [{ rootId: "1", cid: pieceCidHex, rawSize: "1024", ipfsRootCID: null }], - }), - ]), + json: async () => makeSampleResponse([makeSampleRoot({ cid: "0xdeadbeef" })]), }); - const pieces = await service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); - expect(pieces[0].ipfsRootCid).toBeNull(); - expect(pieces[0].withIPFSIndexing).toBe(false); + const piece = await service.sampleAnonPiece(defaultSampleParams); + expect(piece).toBeNull(); }); it("throws after max retries on repeated HTTP errors", async () => { fetchMock.mockResolvedValue({ ok: false, status: 500, statusText: "Internal Server Error" }); - const promise = service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER); + const promise = service.sampleAnonPiece(defaultSampleParams); promise.catch(() => {}); await vi.runAllTimersAsync(); - await expect(promise).rejects.toThrow("Failed to fetch subgraph fwss_candidate_pieces after 3 attempts"); + await expect(promise).rejects.toThrow("Failed to fetch subgraph sample_anon_piece_indexed after 3 attempts"); expect(fetchMock).toHaveBeenCalledTimes(3); }); it("does not retry on schema validation failure", async () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: async () => ({ data: { _meta: { block: { number: 1 } } } }), // missing dataSets + json: async () => ({ data: { _meta: { block: { number: 1 } } } }), // missing roots }); - await expect(service.listFwssCandidatePieces(FWSS_SP_ADDRESS, FWSS_PAYER)).rejects.toThrow(/validation failed/i); + await expect(service.sampleAnonPiece(defaultSampleParams)).rejects.toThrow(/validation failed/i); expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/backend/src/subgraph/subgraph.service.ts b/apps/backend/src/subgraph/subgraph.service.ts index ef73d359..233271f8 100644 --- a/apps/backend/src/subgraph/subgraph.service.ts +++ b/apps/backend/src/subgraph/subgraph.service.ts @@ -4,20 +4,39 @@ import { toStructuredError } from "../common/logging.js"; import type { IBlockchainConfig, IConfig } from "../config/app.config.js"; import { Queries } from "./queries.js"; import type { - FwssCandidatePiece, + AnonCandidatePiece, GraphQLResponse, ProviderDataSetResponse, ProvidersWithDataSetsOptions, - RawCandidatePiecesResponse, + RawSampleAnonPieceResponse, SubgraphMeta, } from "./types.js"; import { decodePieceCid, - validateCandidatePiecesResponse, validateProviderDataSetResponse, + validateSampleAnonPieceResponse, validateSubgraphMetaResponse, } from "./types.js"; +/** Pool of pieces to sample from. */ +export type AnonPiecePool = "indexed" | "any"; + +/** Inputs for a single anonymous piece sample query. */ +export type SampleAnonPieceParams = { + /** Service provider address (lowercase hex). */ + serviceProvider: string; + /** Dealbot's own payer address (excluded to keep the sample non-dealbot). */ + payer: string; + /** Uniform-random 32-byte sort key as `0x`-prefixed hex. */ + sampleKey: string; + /** Inclusive lower bound on raw piece size in bytes (decimal string). */ + minSize: string; + /** Inclusive upper bound on raw piece size in bytes (decimal string). */ + maxSize: string; + /** Which pool to sample from. */ + pool: AnonPiecePool; +}; + /** * Error thrown when data validation fails. * These errors should not be retried as they indicate schema/data issues. @@ -43,11 +62,6 @@ export class SubgraphService { private static readonly MAX_RETRIES = 3; private static readonly INITIAL_RETRY_DELAY_MS = 1000; - /** Max active FWSS datasets fetched per SP per candidate-pieces query. */ - private static readonly FWSS_DATASET_LIMIT = 100; - /** Max pieces fetched per dataset in the candidate-pieces query. */ - private static readonly FWSS_PIECE_LIMIT = 50; - private requestTimestamps: number[] = []; constructor(private readonly configService: ConfigService) { @@ -161,69 +175,65 @@ export class SubgraphService { } /** - * List FWSS candidate pieces for anonymous retrieval testing against the given SP. + * Draw a single random anonymous piece for retrieval testing. * - * Queries for active FWSS datasets owned by `spAddress`, excluding those paid for - * by `dealbotPayer` (the dealbot's own datasets). Datasets whose `pdpPaymentEndEpoch` - * has already passed the latest indexed block are filtered out client-side — the - * subgraph does not flip `isActive` for payment termination. + * Uses the Root.sampleKey (keccak256 of the entity id) to pick the + * smallest key ≥ `params.sampleKey` that matches the filters — a uniform + * random pick when `sampleKey` is generated uniformly. Server-side filters + * cover SP, payer-exclusion, active status, size range, and optionally + * `withIPFSIndexing`. Returns null when no piece matches (callers should + * retry with a fresh sampleKey or relax the pool/bucket). * - * Returns an empty array if the subgraph endpoint is not configured. + * `pdpPaymentEndEpoch` is returned to the caller for a cheap client-side + * epoch comparison — GraphQL filters on nullable BigInts are awkward. */ - async listFwssCandidatePieces(spAddress: string, dealbotPayer: string): Promise { + async sampleAnonPiece(params: SampleAnonPieceParams): Promise { if (!this.blockchainConfig.subgraphEndpoint) { - return []; + return null; } + const query = params.pool === "indexed" ? Queries.SAMPLE_ANON_PIECE_INDEXED : Queries.SAMPLE_ANON_PIECE_ANY; const variables = { - serviceProvider: spAddress.toLowerCase(), - payer: dealbotPayer.toLowerCase(), - datasetLimit: SubgraphService.FWSS_DATASET_LIMIT, - pieceLimit: SubgraphService.FWSS_PIECE_LIMIT, + serviceProvider: params.serviceProvider.toLowerCase(), + payer: params.payer.toLowerCase(), + sampleKey: params.sampleKey, + minSize: params.minSize, + maxSize: params.maxSize, }; - const validated = await this.executeQuery( - "fwss_candidate_pieces", - Queries.GET_FWSS_CANDIDATE_PIECES, + const validated = await this.executeQuery( + `sample_anon_piece_${params.pool}`, + query, variables, - validateCandidatePiecesResponse, + validateSampleAnonPieceResponse, ); - return this.toCandidatePieces(validated); - } - - private toCandidatePieces(response: RawCandidatePiecesResponse): FwssCandidatePiece[] { - const currentEpoch = BigInt(response._meta.block.number); - const pieces: FwssCandidatePiece[] = []; - - for (const ds of response.dataSets) { - if (ds.pdpPaymentEndEpoch != null && BigInt(ds.pdpPaymentEndEpoch) <= currentEpoch) { - continue; - } - - for (const r of ds.roots) { - try { - pieces.push({ - pieceCid: decodePieceCid(r.cid), - pieceId: r.rootId, - dataSetId: ds.setId, - rawSize: r.rawSize, - withIPFSIndexing: ds.withIPFSIndexing, - ipfsRootCid: r.ipfsRootCID ?? null, - }); - } catch (error) { - this.logger.warn({ - event: "fwss_piece_cid_decode_failed", - message: "Failed to decode piece CID from subgraph data", - dataSetId: ds.setId, - pieceId: r.rootId, - error: toStructuredError(error), - }); - } - } + const root = validated.roots[0]; + if (!root) { + return null; } - return pieces; + try { + return { + pieceCid: decodePieceCid(root.cid), + pieceId: root.rootId, + dataSetId: root.proofSet.setId, + rawSize: root.rawSize, + withIPFSIndexing: root.proofSet.withIPFSIndexing, + ipfsRootCid: root.ipfsRootCID ?? null, + indexedAtBlock: validated._meta.block.number, + pdpPaymentEndEpoch: root.proofSet.pdpPaymentEndEpoch != null ? BigInt(root.proofSet.pdpPaymentEndEpoch) : null, + }; + } catch (error) { + this.logger.warn({ + event: "anon_piece_cid_decode_failed", + message: "Failed to decode piece CID from subgraph data", + dataSetId: root.proofSet.setId, + pieceId: root.rootId, + error: toStructuredError(error), + }); + return null; + } } /** @@ -420,9 +430,7 @@ export class SubgraphService { addressCount: addresses.length, error: toStructuredError(error), }); - throw new Error( - `Failed to fetch provider data after ${SubgraphService.MAX_RETRIES} attempts: ${errorMessage}`, - ); + throw new Error(`Failed to fetch provider data after ${SubgraphService.MAX_RETRIES} attempts: ${errorMessage}`); } } diff --git a/apps/backend/src/subgraph/types.ts b/apps/backend/src/subgraph/types.ts index ffb66e57..3a89f360 100644 --- a/apps/backend/src/subgraph/types.ts +++ b/apps/backend/src/subgraph/types.ts @@ -56,7 +56,7 @@ export type ProviderDataSetResponse = { }; /** A piece eligible for anonymous retrieval. */ -export type FwssCandidatePiece = { +export type AnonCandidatePiece = { /** Decoded piece CID string (e.g. "bafk..."). */ pieceCid: string; /** On-chain piece ID (rootId) as a decimal string. */ @@ -69,24 +69,29 @@ export type FwssCandidatePiece = { withIPFSIndexing: boolean; /** IPFS root CID declared by the client when uploading, or null. */ ipfsRootCid: string | null; + /** Subgraph-indexed block number at query time. */ + indexedAtBlock: number; + /** pdpPaymentEndEpoch from the parent dataset, or null. */ + pdpPaymentEndEpoch: bigint | null; }; /** - * Validated raw shape of the FWSS candidate-pieces subgraph response. - * Consumers should prefer the parsed FwssCandidatePiece[] output. + * Validated raw shape of the anonymous piece sampling subgraph response. + * At most one root is returned (`first: 1`). */ -export type RawCandidatePiecesResponse = { +export type RawSampleAnonPieceResponse = { _meta: { block: { number: number } }; - dataSets: Array<{ - setId: string; - withIPFSIndexing: boolean; - pdpPaymentEndEpoch: string | null; - roots: Array<{ - rootId: string; - cid: string; - rawSize: string; - ipfsRootCID: string | null; - }>; + roots: Array<{ + rootId: string; + cid: string; + rawSize: string; + ipfsRootCID: string | null; + proofSet: { + setId: string; + withIPFSIndexing: boolean; + fwssPayer: string | null; + pdpPaymentEndEpoch: string | null; + }; }>; }; @@ -165,23 +170,27 @@ const providerDataSetResponseSchema = Joi.object({ .unknown(true) .required(); -const candidateRootSchema = Joi.object({ +const sampleRootProofSetSchema = Joi.object({ + setId: Joi.string().pattern(/^\d+$/).required(), + withIPFSIndexing: Joi.boolean().required(), + fwssPayer: Joi.string() + .pattern(/^0x[0-9a-fA-F]{40}$/) + .allow(null) + .optional(), + pdpPaymentEndEpoch: Joi.string().pattern(/^\d+$/).allow(null).optional(), +}).unknown(true); + +const sampleRootSchema = Joi.object({ rootId: Joi.string().pattern(/^\d+$/).required(), cid: Joi.string() .pattern(/^0x[0-9a-fA-F]+$/) .required(), rawSize: Joi.string().pattern(/^\d+$/).required(), ipfsRootCID: Joi.string().allow(null).optional(), + proofSet: sampleRootProofSetSchema.required(), }).unknown(true); -const candidateDataSetSchema = Joi.object({ - setId: Joi.string().pattern(/^\d+$/).required(), - withIPFSIndexing: Joi.boolean().required(), - pdpPaymentEndEpoch: Joi.string().pattern(/^\d+$/).allow(null).optional(), - roots: Joi.array().items(candidateRootSchema).required(), -}).unknown(true); - -const candidatePiecesResponseSchema = Joi.object({ +const sampleAnonPieceResponseSchema = Joi.object({ _meta: Joi.object({ block: Joi.object({ number: Joi.number().integer().positive().required(), @@ -191,7 +200,7 @@ const candidatePiecesResponseSchema = Joi.object({ }) .unknown(true) .required(), - dataSets: Joi.array().items(candidateDataSetSchema).required(), + roots: Joi.array().items(sampleRootSchema).max(1).required(), }) .unknown(true) .required(); @@ -230,14 +239,14 @@ export function validateProviderDataSetResponse(value: unknown): ProviderDataSet } /** - * Validates the raw FWSS candidate-pieces response from the subgraph. + * Validates the raw sampleAnonPiece response from the subgraph. * * @throws Error if validation fails */ -export function validateCandidatePiecesResponse(value: unknown): RawCandidatePiecesResponse { - const { error, value: validated } = candidatePiecesResponseSchema.validate(value, { abortEarly: false }); +export function validateSampleAnonPieceResponse(value: unknown): RawSampleAnonPieceResponse { + const { error, value: validated } = sampleAnonPieceResponseSchema.validate(value, { abortEarly: false }); if (error) { - throw new Error(`Invalid candidate pieces response format: ${error.message}`); + throw new Error(`Invalid sampleAnonPiece response format: ${error.message}`); } - return validated as RawCandidatePiecesResponse; + return validated as RawSampleAnonPieceResponse; } diff --git a/apps/subgraph/schema.graphql b/apps/subgraph/schema.graphql index 63a5fd79..91eadf36 100644 --- a/apps/subgraph/schema.graphql +++ b/apps/subgraph/schema.graphql @@ -72,5 +72,11 @@ type Root @entity(immutable: false) { # Set once on creation and never updated. createdAt: BigInt! + # keccak256(id) computed once at insert time. Used by dealbot's anonymous + # retrieval check to draw a uniform-random piece via + # `orderBy: sampleKey, where: { sampleKey_gte: }, first: 1` + # — independent of creation order, dataset age, and corpus size. + sampleKey: Bytes! + proofSet: DataSet! } diff --git a/apps/subgraph/src/helpers.ts b/apps/subgraph/src/helpers.ts index 01e0c8f6..e3350bdb 100644 --- a/apps/subgraph/src/helpers.ts +++ b/apps/subgraph/src/helpers.ts @@ -1,4 +1,4 @@ -import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { Address, BigInt, Bytes, crypto } from "@graphprotocol/graph-ts"; // ---- Entity ID helpers ---------------------------------------------------- @@ -10,6 +10,13 @@ export function getRootEntityId(setId: BigInt, rootId: BigInt): Bytes { return Bytes.fromUTF8(setId.toString() + "-" + rootId.toString()); } +// Uniform pseudorandom sort key for Root entities. Used by dealbot to draw +// random pieces fairly via `orderBy: sampleKey, where: { sampleKey_gte: X }`, +// which needs a key distributed independently of setId/rootId. +export function getRootSampleKey(rootEntityId: Bytes): Bytes { + return Bytes.fromByteArray(crypto.keccak256(rootEntityId)); +} + // ---- FWSS metadata helpers ------------------------------------------------ export function arrayContains(arr: string[], needle: string): boolean { diff --git a/apps/subgraph/src/pdp-verifier.ts b/apps/subgraph/src/pdp-verifier.ts index 65f0f710..fd99ed9e 100644 --- a/apps/subgraph/src/pdp-verifier.ts +++ b/apps/subgraph/src/pdp-verifier.ts @@ -13,6 +13,7 @@ import { DataSet, Provider, Root } from "../generated/schema"; import { getProofSetEntityId, getRootEntityId, + getRootSampleKey, maxProvingPeriodFor, unpaddedSize, validateCommPv2, @@ -200,6 +201,7 @@ export function handlePiecesAdded(event: PiecesAddedEvent): void { root.removed = false; root.createdAt = event.block.timestamp; root.proofSet = getProofSetEntityId(setId); + root.sampleKey = getRootSampleKey(rootEntityId); // ipfsRootCID: patched in FWSS handler if applicable. root.save(); diff --git a/apps/subgraph/subgraph.yaml b/apps/subgraph/subgraph.yaml index 6f36ecdb..bf9fb696 100644 --- a/apps/subgraph/subgraph.yaml +++ b/apps/subgraph/subgraph.yaml @@ -6,11 +6,11 @@ schema: dataSources: - kind: ethereum name: PDPVerifier - network: filecoin + network: filecoin-testnet source: abi: PDPVerifier - address: "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F" - startBlock: 5441432 + address: "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C" + startBlock: 3140755 mapping: kind: ethereum/events apiVersion: 0.0.9 @@ -42,11 +42,11 @@ dataSources: handler: handleNextProvingPeriod - kind: ethereum name: FilecoinWarmStorageService - network: filecoin + network: filecoin-testnet source: abi: FilecoinWarmStorageService - address: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f" - startBlock: 5459617 + address: "0x02925630df557F957f70E112bA06e50965417CA0" + startBlock: 3141276 mapping: kind: ethereum/events apiVersion: 0.0.9 diff --git a/apps/subgraph/tests/pdp-verifier.test.ts b/apps/subgraph/tests/pdp-verifier.test.ts index 444adc32..1c0ece7a 100644 --- a/apps/subgraph/tests/pdp-verifier.test.ts +++ b/apps/subgraph/tests/pdp-verifier.test.ts @@ -1,6 +1,6 @@ import { afterAll, assert, beforeAll, clearStore, describe, test } from "matchstick-as/assembly/index"; import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; -import { getRootEntityId } from "../src/helpers"; +import { getRootEntityId, getRootSampleKey } from "../src/helpers"; import { handleDataSetCreated, handlePiecesAdded } from "../src/pdp-verifier"; import { createDataSetCreatedEvent, createRootsAddedEvent } from "./pdp-verifier-utils"; @@ -49,12 +49,14 @@ describe("handlePiecesAdded Tests", () => { assert.fieldEquals("DataSet", dataSetId, "isActive", "true"); assert.fieldEquals("DataSet", dataSetId, "owner", SENDER_ADDRESS.toHex()); - const rootEntityId = getRootEntityId(SET_ID, ROOT_ID_1).toHex(); + const rootEntityIdBytes = getRootEntityId(SET_ID, ROOT_ID_1); + const rootEntityId = rootEntityIdBytes.toHex(); assert.fieldEquals("Root", rootEntityId, "rootId", ROOT_ID_1.toString()); assert.fieldEquals("Root", rootEntityId, "setId", SET_ID.toString()); assert.fieldEquals("Root", rootEntityId, "cid", ROOT_CID_1_STR); assert.fieldEquals("Root", rootEntityId, "rawSize", RAW_SIZE_1.toString()); assert.fieldEquals("Root", rootEntityId, "removed", "false"); + assert.fieldEquals("Root", rootEntityId, "sampleKey", getRootSampleKey(rootEntityIdBytes).toHex()); const providerId = SENDER_ADDRESS.toHex(); assert.fieldEquals("Provider", providerId, "address", providerId); From 743ec1734b631f89dd4f57f0296ebb5ae6790891 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Wed, 22 Apr 2026 18:31:49 +0200 Subject: [PATCH 10/19] fix: failing tests from rebasing --- .../data-retention.service.spec.ts | 10 +++++----- apps/backend/src/jobs/jobs.service.spec.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/data-retention/data-retention.service.spec.ts b/apps/backend/src/data-retention/data-retention.service.spec.ts index 254a9707..cf74d9ad 100644 --- a/apps/backend/src/data-retention/data-retention.service.spec.ts +++ b/apps/backend/src/data-retention/data-retention.service.spec.ts @@ -153,26 +153,26 @@ describe("DataRetentionService", () => { it("returns early when all providers are blocked for data-retention", async () => { (configServiceMock.get as ReturnType).mockImplementation((key: string) => { - if (key === "blockchain") return { pdpSubgraphEndpoint: "https://example.com/subgraph" }; + if (key === "blockchain") return { subgraphEndpoint: "https://example.com/subgraph" }; if (key === "spBlocklists") return { ids: new Set(), addresses: new Set([PROVIDER_A, PROVIDER_B]) }; }); await service.pollDataRetention(); - expect(pdpSubgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); + expect(subgraphServiceMock.fetchProvidersWithDatasets).not.toHaveBeenCalled(); }); it("excludes blocked providers from data-retention polling while retaining unblocked ones", async () => { (configServiceMock.get as ReturnType).mockImplementation((key: string) => { - if (key === "blockchain") return { pdpSubgraphEndpoint: "https://example.com/subgraph" }; + if (key === "blockchain") return { subgraphEndpoint: "https://example.com/subgraph" }; if (key === "spBlocklists") return { ids: new Set(), addresses: new Set([PROVIDER_A]) }; }); - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); + subgraphServiceMock.fetchProvidersWithDatasets.mockResolvedValueOnce([makeProvider({ address: PROVIDER_B })]); await service.pollDataRetention(); const allAddressesPolled: string[] = ( - pdpSubgraphServiceMock.fetchProvidersWithDatasets.mock.calls as [{ addresses: string[] }][] + subgraphServiceMock.fetchProvidersWithDatasets.mock.calls as [{ addresses: string[] }][] ).flatMap(([{ addresses }]) => addresses); expect(allAddressesPolled).toContain(PROVIDER_B.toLowerCase()); expect(allAddressesPolled).not.toContain(PROVIDER_A.toLowerCase()); diff --git a/apps/backend/src/jobs/jobs.service.spec.ts b/apps/backend/src/jobs/jobs.service.spec.ts index 1206df17..aaa1c41f 100644 --- a/apps/backend/src/jobs/jobs.service.spec.ts +++ b/apps/backend/src/jobs/jobs.service.spec.ts @@ -968,8 +968,8 @@ describe("JobsService schedule rows", () => { service = buildService({ dealService: dealService as unknown as ConstructorParameters[3], - walletSdkService: walletSdkService as unknown as ConstructorParameters[5], - pieceCleanupService: pieceCleanupService as unknown as JobsServiceDeps[7], + walletSdkService: walletSdkService as unknown as ConstructorParameters[6], + pieceCleanupService: pieceCleanupService as unknown as JobsServiceDeps[8], }); await callPrivate(service, "handleDealJob", { @@ -1472,7 +1472,7 @@ describe("JobsService schedule rows", () => { service = buildService({ dealService: dealService as unknown as JobsServiceDeps[3], - walletSdkService: walletSdkService as unknown as JobsServiceDeps[5], + walletSdkService: walletSdkService as unknown as JobsServiceDeps[6], }); await callPrivate(service, "handleDealJob", { @@ -1496,7 +1496,7 @@ describe("JobsService schedule rows", () => { service = buildService({ retrievalService: retrievalService as unknown as JobsServiceDeps[4], - walletSdkService: walletSdkService as unknown as JobsServiceDeps[5], + walletSdkService: walletSdkService as unknown as JobsServiceDeps[6], }); await callPrivate(service, "handleRetrievalJob", { @@ -1524,7 +1524,7 @@ describe("JobsService schedule rows", () => { service = buildService({ dealService: dealService as unknown as JobsServiceDeps[3], - walletSdkService: walletSdkService as unknown as JobsServiceDeps[5], + walletSdkService: walletSdkService as unknown as JobsServiceDeps[6], }); await callPrivate(service, "handleDataSetCreationJob", { @@ -1565,7 +1565,7 @@ describe("JobsService schedule rows", () => { intervalSeconds: 60, service: buildService({ dealService: dealService as unknown as JobsServiceDeps[3], - walletSdkService: walletSdkService as unknown as JobsServiceDeps[5], + walletSdkService: walletSdkService as unknown as JobsServiceDeps[6], }), expectCheckNotRun: () => expect(dealService.createDealForProvider).not.toHaveBeenCalled(), }, @@ -1575,7 +1575,7 @@ describe("JobsService schedule rows", () => { intervalSeconds: 60, service: buildService({ retrievalService: retrievalService as unknown as JobsServiceDeps[4], - walletSdkService: walletSdkService as unknown as JobsServiceDeps[5], + walletSdkService: walletSdkService as unknown as JobsServiceDeps[6], }), expectCheckNotRun: () => expect(retrievalService.performRandomRetrievalForProvider).not.toHaveBeenCalled(), }, @@ -1585,7 +1585,7 @@ describe("JobsService schedule rows", () => { intervalSeconds: 3600, service: buildService({ dealService: dataSetDealService as unknown as JobsServiceDeps[3], - walletSdkService: walletSdkService as unknown as JobsServiceDeps[5], + walletSdkService: walletSdkService as unknown as JobsServiceDeps[6], }), expectCheckNotRun: () => expect(dataSetDealService.createDataSetWithPiece).not.toHaveBeenCalled(), }, From a67361364312fb402598155ece8692867b781ff2 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 08:27:19 +0200 Subject: [PATCH 11/19] fix(ci): pnpm script handling --- .github/workflows/subgraph.yml | 47 ---------------------------------- .github/workflows/test.yml | 3 +++ apps/subgraph/package.json | 1 + apps/subgraph/subgraph.yaml | 12 ++++----- 4 files changed, 10 insertions(+), 53 deletions(-) delete mode 100644 .github/workflows/subgraph.yml diff --git a/.github/workflows/subgraph.yml b/.github/workflows/subgraph.yml deleted file mode 100644 index 1fe39343..00000000 --- a/.github/workflows/subgraph.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Subgraph - -on: - pull_request: - paths: - - 'apps/subgraph/**' - - 'pnpm-lock.yaml' - - 'pnpm-workspace.yaml' - - '.github/workflows/subgraph.yml' - -jobs: - build-and-test: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.2 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build subgraph (mainnet) - run: pnpm --filter @dealbot/subgraph build:mainnet - - - name: Build subgraph (calibnet) - run: pnpm --filter @dealbot/subgraph build:calibnet - - - name: Restore mainnet manifest - # graph build --network X rewrites subgraph.yaml in place. Re-run the - # mainnet build to leave the checked-in file with mainnet defaults. - run: pnpm --filter @dealbot/subgraph build:mainnet - - - name: Run matchstick tests - run: pnpm --filter @dealbot/subgraph test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 835e8083..af7afde5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,6 +79,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Codegen subgraph + run: pnpm --filter @dealbot/subgraph codegen + - name: Run unit tests run: pnpm test diff --git a/apps/subgraph/package.json b/apps/subgraph/package.json index 3a9a87e5..90e70cc7 100644 --- a/apps/subgraph/package.json +++ b/apps/subgraph/package.json @@ -4,6 +4,7 @@ "license": "(MIT OR Apache-2.0)", "scripts": { "codegen": "graph codegen", + "build": "pnpm run build:mainnet", "build:mainnet": "graph codegen && graph build --network filecoin", "build:calibration": "graph codegen && graph build --network filecoin-testnet", "deploy:mainnet": "goldsky subgraph deploy dealbot-mainnet/$VERSION --path ./build", diff --git a/apps/subgraph/subgraph.yaml b/apps/subgraph/subgraph.yaml index bf9fb696..6f36ecdb 100644 --- a/apps/subgraph/subgraph.yaml +++ b/apps/subgraph/subgraph.yaml @@ -6,11 +6,11 @@ schema: dataSources: - kind: ethereum name: PDPVerifier - network: filecoin-testnet + network: filecoin source: abi: PDPVerifier - address: "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C" - startBlock: 3140755 + address: "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F" + startBlock: 5441432 mapping: kind: ethereum/events apiVersion: 0.0.9 @@ -42,11 +42,11 @@ dataSources: handler: handleNextProvingPeriod - kind: ethereum name: FilecoinWarmStorageService - network: filecoin-testnet + network: filecoin source: abi: FilecoinWarmStorageService - address: "0x02925630df557F957f70E112bA06e50965417CA0" - startBlock: 3141276 + address: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f" + startBlock: 5459617 mapping: kind: ethereum/events apiVersion: 0.0.9 From 198174e68e22963c6cbc8eaa10cb84a0f515be76 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 09:08:28 +0200 Subject: [PATCH 12/19] refactor: pull request self review --- apps/backend/src/jobs/jobs.service.ts | 34 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/jobs/jobs.service.ts b/apps/backend/src/jobs/jobs.service.ts index 3ff48bfb..3e0505a5 100644 --- a/apps/backend/src/jobs/jobs.service.ts +++ b/apps/backend/src/jobs/jobs.service.ts @@ -18,11 +18,22 @@ import { RetrievalService } from "../retrieval/retrieval.service.js"; import { AnonRetrievalService } from "../retrieval-anon/anon-retrieval.service.js"; import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; import { provisionNextMissingDataSet } from "./data-set-creation.handler.js"; -import { DATA_RETENTION_POLL_QUEUE, PROVIDERS_REFRESH_QUEUE, RETRIEVAL_ANON_QUEUE,SP_WORK_QUEUE } from "./job-queues.js"; +import { + DATA_RETENTION_POLL_QUEUE, + PROVIDERS_REFRESH_QUEUE, + RETRIEVAL_ANON_QUEUE, + SP_WORK_QUEUE, +} from "./job-queues.js"; import { JobScheduleRepository } from "./repositories/job-schedule.repository.js"; type SpJobType = "deal" | "retrieval" | "data_set_creation" | "retrieval_anon" | "piece_cleanup"; -const SP_JOB_TYPES: ReadonlySet = new Set(["deal", "retrieval", "retrieval_anon", "data_set_creation", "piece_cleanup"]); +const SP_JOB_TYPES: ReadonlySet = new Set([ + "deal", + "retrieval", + "retrieval_anon", + "data_set_creation", + "piece_cleanup", +]); function isSpJobType(jobType: string): jobType is SpJobType { return SP_JOB_TYPES.has(jobType); @@ -30,7 +41,6 @@ function isSpJobType(jobType: string): jobType is SpJobType { type SpJobData = { jobType: SpJobType; spAddress: string; intervalSeconds: number }; type AnonRetrievalJobData = { spAddress: string; intervalSeconds: number }; -type MetricsJobData = { intervalSeconds: number }; type ProvidersRefreshJobData = { intervalSeconds: number }; type SpJob = Job; type DataRetentionJobData = { intervalSeconds: number }; @@ -906,7 +916,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { private async deferJobForMaintenance( jobType: SpJobType, - data: SpJobData | AnonRetrievalJobData, + data: SpJobData, maintenance: ReturnType, now: Date, ): Promise { @@ -914,8 +924,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { if (resumeAt == null) { return; } - const queueName = jobType === "retrieval_anon" ? RETRIEVAL_ANON_QUEUE : SP_WORK_QUEUE; - await this.safeSend(jobType, queueName, data, { startAfter: resumeAt }); + await this.safeSend(jobType, SP_WORK_QUEUE, data, { startAfter: resumeAt }); } /** @@ -1238,12 +1247,15 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { } private mapJobPayload(row: ScheduleRow): SpJobData | ProvidersRefreshJobData | DataRetentionJobData { - if (row.job_type === "deal" || row.job_type === "retrieval" || row.job_type === "data_set_creation" || row.job_type === "piece_cleanup") { + if ( + row.job_type === "deal" || + row.job_type === "retrieval" || + row.job_type === "retrieval_anon" || + row.job_type === "data_set_creation" || + row.job_type === "piece_cleanup" + ) { return { jobType: row.job_type, spAddress: row.sp_address, intervalSeconds: row.interval_seconds }; } - if (row.job_type === "retrieval_anon") { - return { spAddress: row.sp_address, intervalSeconds: row.interval_seconds }; - } return { intervalSeconds: row.interval_seconds }; } @@ -1261,7 +1273,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { // Disable retries so "attempted" jobs don't rerun; failures are handled by the next schedule tick. const finalOptions: SendOptions = { retryLimit: 0, ...options }; if (isSpJobType(jobType)) { - const spData = data as { spAddress: string }; + const spData = data as SpJobData; if (!finalOptions.singletonKey) { finalOptions.singletonKey = spData.spAddress; } From b214a03895fa5d71ccfd42861afc70b02410c61b Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 11:14:01 +0200 Subject: [PATCH 13/19] change: decrease size bucket limits --- .../src/retrieval-anon/anon-piece-selector.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts index 6744bfc3..12d4e337 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts @@ -27,12 +27,12 @@ type SizeBucket = "small" | "medium" | "large"; type SizeRange = { min: bigint; max: bigint }; const MIB = 1024n * 1024n; -const GIB = 1024n * MIB; +// All downloads are buffered in-memory, so we need to keep piece sizes reasonable const SIZE_BUCKETS: Record = { - small: { min: 1n, max: 64n * MIB - 1n }, - medium: { min: 64n * MIB, max: 1n * GIB - 1n }, - large: { min: 1n * GIB, max: 32n * GIB }, + small: { min: 1n * MIB, max: 20n * MIB - 1n }, + medium: { min: 20n * MIB, max: 100n * MIB - 1n }, + large: { min: 100n * MIB, max: 500n * MIB - 1n }, }; /** Weights for choosing a bucket per selection. Must sum to 1. */ From ee65c7e1f0a8a176dd81e53177de82794ff88af8 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 11:42:48 +0200 Subject: [PATCH 14/19] refactor(retrieval-anon): use dedicated anonymous retrieval table --- apps/backend/src/database/database.module.ts | 9 +- .../entities/anon-retrieval.entity.ts | 96 +++++++++++++++++++ .../src/database/entities/retrieval.entity.ts | 24 +---- .../1762000000000-AddAnonRetrievalColumns.ts | 67 ------------- .../1762000000000-CreateAnonRetrievals.ts | 63 ++++++++++++ .../anon-piece-selector.service.spec.ts | 10 +- .../anon-piece-selector.service.ts | 16 ++-- .../retrieval-anon/anon-retrieval.service.ts | 26 ++--- .../retrieval-anon/retrieval-anon.module.ts | 4 +- 9 files changed, 194 insertions(+), 121 deletions(-) create mode 100644 apps/backend/src/database/entities/anon-retrieval.entity.ts delete mode 100644 apps/backend/src/database/migrations/1762000000000-AddAnonRetrievalColumns.ts create mode 100644 apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts diff --git a/apps/backend/src/database/database.module.ts b/apps/backend/src/database/database.module.ts index 9249c3a9..f3f9ed09 100644 --- a/apps/backend/src/database/database.module.ts +++ b/apps/backend/src/database/database.module.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from "url"; import { toStructuredError } from "../common/logging.js"; import { createPinoExitLogger } from "../common/pino.config.js"; import type { IAppConfig, IConfig, IDatabaseConfig } from "../config/app.config.js"; +import { AnonRetrieval } from "./entities/anon-retrieval.entity.js"; import { DataRetentionBaseline } from "./entities/data-retention-baseline.entity.js"; import { Deal } from "./entities/deal.entity.js"; import { JobScheduleState } from "./entities/job-schedule-state.entity.js"; @@ -49,7 +50,7 @@ function toSafeDataSourceContext(options: DataSourceOptions): Record { - // Make deal_id nullable — anonymous retrievals have no dealbot deal. - await queryRunner.query(` - ALTER TABLE retrievals - ALTER COLUMN deal_id DROP NOT NULL - `); - - await queryRunner.query(` - ALTER TABLE retrievals - ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN NOT NULL DEFAULT FALSE - `); - - await queryRunner.query(` - ALTER TABLE retrievals - ADD COLUMN IF NOT EXISTS anon_piece_cid TEXT DEFAULT NULL - `); - - await queryRunner.query(` - ALTER TABLE retrievals - ADD COLUMN IF NOT EXISTS anon_data_set_id TEXT DEFAULT NULL - `); - - await queryRunner.query(` - ALTER TABLE retrievals - ADD COLUMN IF NOT EXISTS anon_piece_id TEXT DEFAULT NULL - `); - - // NULL = not checked (e.g., retrieval failed before CommP step) - await queryRunner.query(` - ALTER TABLE retrievals - ADD COLUMN IF NOT EXISTS commp_valid BOOLEAN DEFAULT NULL - `); - - // NULL = not checked (e.g., withIPFSIndexing was false or piece retrieval failed) - await queryRunner.query(` - ALTER TABLE retrievals - ADD COLUMN IF NOT EXISTS car_valid BOOLEAN DEFAULT NULL - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_retrievals_is_anonymous" - ON retrievals (is_anonymous) - WHERE is_anonymous = TRUE - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS "IDX_retrievals_is_anonymous"`); - await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS car_valid`); - await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS commp_valid`); - await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS anon_piece_id`); - await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS anon_data_set_id`); - await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS anon_piece_cid`); - await queryRunner.query(`ALTER TABLE retrievals DROP COLUMN IF EXISTS is_anonymous`); - - // Restore NOT NULL on deal_id (only safe if all anonymous rows are cleaned up first) - await queryRunner.query(` - ALTER TABLE retrievals - ALTER COLUMN deal_id SET NOT NULL - `); - } -} diff --git a/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts b/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts new file mode 100644 index 00000000..3f682099 --- /dev/null +++ b/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts @@ -0,0 +1,63 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Create the `anon_retrievals` table that stores anonymous retrieval check + * records. Kept separate from `retrievals` because the two checks have + * different input domains — `retrievals` is always tied to a dealbot-owned + * deal, while `anon_retrievals` carries its own piece identity inline. + */ +export class CreateAnonRetrievals1762000000000 implements MigrationInterface { + name = "CreateAnonRetrievals1762000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE anon_retrievals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sp_address VARCHAR NOT NULL, + piece_cid VARCHAR NOT NULL, + data_set_id BIGINT NOT NULL, + piece_id BIGINT NOT NULL, + with_ipfs_indexing BOOLEAN NOT NULL, + ipfs_root_cid VARCHAR NULL, + service_type VARCHAR NOT NULL DEFAULT 'direct_sp', + retrieval_endpoint VARCHAR NOT NULL, + status VARCHAR NOT NULL DEFAULT 'pending', + started_at TIMESTAMPTZ NOT NULL, + completed_at TIMESTAMPTZ NULL, + latency_ms INT NULL, + ttfb_ms INT NULL, + throughput_bps INT NULL, + bytes_retrieved BIGINT NULL, + response_code INT NULL, + error_message VARCHAR NULL, + commp_valid BOOLEAN NULL, + car_valid BOOLEAN NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `); + + // Per-SP dashboards. + await queryRunner.query(` + CREATE INDEX "IDX_anon_retrievals_sp_address" + ON anon_retrievals (sp_address) + `); + + // Used by the recent-dedup query in AnonPieceSelectorService — keeps the + // most-recently-tested CIDs out of the next selection. + await queryRunner.query(` + CREATE INDEX "IDX_anon_retrievals_piece_cid" + ON anon_retrievals (piece_cid) + `); + + // Supports "last N anonymous retrievals" ordering used by the selector. + await queryRunner.query(` + CREATE INDEX "IDX_anon_retrievals_created_at" + ON anon_retrievals (created_at DESC) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS anon_retrievals`); + } +} diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts index 78a4e5fd..b822fe5f 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.spec.ts @@ -2,7 +2,7 @@ import type { ConfigService } from "@nestjs/config"; import type { Repository } from "typeorm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { IConfig } from "../config/app.config.js"; -import type { Retrieval } from "../database/entities/retrieval.entity.js"; +import type { AnonRetrieval } from "../database/entities/anon-retrieval.entity.js"; import type { SampleAnonPieceParams, SubgraphService } from "../subgraph/subgraph.service.js"; import type { AnonCandidatePiece } from "../subgraph/types.js"; import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; @@ -22,18 +22,16 @@ const makePiece = (overrides: Partial = {}): AnonCandidatePi ...overrides, }); -const makeRetrievalRepository = (recentPieceCids: string[]): Repository => { +const makeRetrievalRepository = (recentPieceCids: string[]): Repository => { const queryBuilder = { select: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - andWhere: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), - getRawMany: vi.fn().mockResolvedValue(recentPieceCids.map((c) => ({ anonPieceCid: c }))), + getRawMany: vi.fn().mockResolvedValue(recentPieceCids.map((c) => ({ pieceCid: c }))), }; return { createQueryBuilder: vi.fn().mockReturnValue(queryBuilder), - } as unknown as Repository; + } as unknown as Repository; }; const makeConfigService = (): ConfigService => diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts index 12d4e337..2af6f167 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts @@ -4,7 +4,7 @@ import { ConfigService } from "@nestjs/config"; import { InjectRepository } from "@nestjs/typeorm"; import type { Repository } from "typeorm"; import type { IConfig } from "../config/app.config.js"; -import { Retrieval } from "../database/entities/retrieval.entity.js"; +import { AnonRetrieval } from "../database/entities/anon-retrieval.entity.js"; import type { AnonPiecePool, SampleAnonPieceParams } from "../subgraph/subgraph.service.js"; import { SubgraphService } from "../subgraph/subgraph.service.js"; import type { AnonCandidatePiece } from "../subgraph/types.js"; @@ -56,8 +56,8 @@ export class AnonPieceSelectorService { constructor( private readonly subgraphService: SubgraphService, private readonly configService: ConfigService, - @InjectRepository(Retrieval) - private readonly retrievalRepository: Repository, + @InjectRepository(AnonRetrieval) + private readonly anonRetrievalRepository: Repository, ) {} /** @@ -185,16 +185,14 @@ export class AnonPieceSelectorService { * anonymous retrievals across all SPs. */ private async loadRecentlyTestedPieceCids(): Promise> { - const rows = await this.retrievalRepository + const rows = await this.anonRetrievalRepository .createQueryBuilder("r") - .select("r.anon_piece_cid", "anonPieceCid") - .where("r.is_anonymous = true") - .andWhere("r.anon_piece_cid IS NOT NULL") + .select("r.piece_cid", "pieceCid") .orderBy("r.created_at", "DESC") .limit(RECENT_DEDUP_WINDOW) - .getRawMany<{ anonPieceCid: string }>(); + .getRawMany<{ pieceCid: string }>(); - return new Set(rows.map((row) => row.anonPieceCid)); + return new Set(rows.map((row) => row.pieceCid)); } } diff --git a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts index b87962cd..a5b9b673 100644 --- a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts +++ b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import type { Repository } from "typeorm"; import { type ProviderJobContext, toStructuredError } from "../common/logging.js"; -import { Retrieval } from "../database/entities/retrieval.entity.js"; +import { AnonRetrieval } from "../database/entities/anon-retrieval.entity.js"; import { StorageProvider } from "../database/entities/storage-provider.entity.js"; import { RetrievalStatus, ServiceType } from "../database/types.js"; import { buildCheckMetricLabels } from "../metrics-prometheus/check-metric-labels.js"; @@ -23,8 +23,8 @@ export class AnonRetrievalService { private readonly carValidationService: CarValidationService, private readonly walletSdkService: WalletSdkService, private readonly metrics: AnonRetrievalCheckMetrics, - @InjectRepository(Retrieval) - private readonly retrievalRepository: Repository, + @InjectRepository(AnonRetrieval) + private readonly anonRetrievalRepository: Repository, @InjectRepository(StorageProvider) private readonly spRepository: Repository, ) {} @@ -33,7 +33,7 @@ export class AnonRetrievalService { spAddress: string, signal?: AbortSignal, logContext?: ProviderJobContext, - ): Promise { + ): Promise { // Build metric labels const provider = await this.spRepository.findOne({ where: { address: spAddress } }); const labels = buildCheckMetricLabels({ @@ -129,11 +129,13 @@ export class AnonRetrievalService { const spBaseUrl = providerInfo?.pdp.serviceURL.replace(/\/$/, "") ?? spAddress; // 5. Save retrieval record - const retrieval = this.retrievalRepository.create({ - isAnonymous: true, - anonPieceCid: piece.pieceCid, - anonDataSetId: piece.dataSetId, - anonPieceId: piece.pieceId, + const retrieval = this.anonRetrievalRepository.create({ + spAddress, + pieceCid: piece.pieceCid, + dataSetId: BigInt(piece.dataSetId), + pieceId: BigInt(piece.pieceId), + withIpfsIndexing: piece.withIPFSIndexing, + ipfsRootCid: piece.ipfsRootCid, serviceType: ServiceType.DIRECT_SP, retrievalEndpoint: `${spBaseUrl}/piece/${piece.pieceCid}`, status: pieceResult.success ? RetrievalStatus.SUCCESS : RetrievalStatus.FAILED, @@ -145,12 +147,12 @@ export class AnonRetrievalService { bytesRetrieved: pieceResult.bytesReceived, responseCode: pieceResult.statusCode, errorMessage: pieceResult.errorMessage, - commPValid: pieceResult.success ? pieceResult.commPValid : undefined, - carValid: carResult ? carResult.ipniValid !== false && carResult.blockFetchValid !== false : undefined, + commpValid: pieceResult.success ? pieceResult.commPValid : null, + carValid: carResult ? carResult.ipniValid !== false && carResult.blockFetchValid !== false : null, }); try { - await this.retrievalRepository.save(retrieval); + await this.anonRetrievalRepository.save(retrieval); } catch (error) { this.logger.warn({ ...logContext, diff --git a/apps/backend/src/retrieval-anon/retrieval-anon.module.ts b/apps/backend/src/retrieval-anon/retrieval-anon.module.ts index 08210103..4e9e38df 100644 --- a/apps/backend/src/retrieval-anon/retrieval-anon.module.ts +++ b/apps/backend/src/retrieval-anon/retrieval-anon.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { Retrieval } from "../database/entities/retrieval.entity.js"; +import { AnonRetrieval } from "../database/entities/anon-retrieval.entity.js"; import { StorageProvider } from "../database/entities/storage-provider.entity.js"; import { HttpClientModule } from "../http-client/http-client.module.js"; import { IpniModule } from "../ipni/ipni.module.js"; @@ -15,7 +15,7 @@ import { PieceRetrievalService } from "./piece-retrieval.service.js"; @Module({ imports: [ ConfigModule, - TypeOrmModule.forFeature([Retrieval, StorageProvider]), + TypeOrmModule.forFeature([AnonRetrieval, StorageProvider]), SubgraphModule, WalletSdkModule, HttpClientModule, From 744b3a459ae11165d7bdd417a283cef69897420f Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 12:05:02 +0200 Subject: [PATCH 15/19] add(retrieval-anon): raw size column --- .../entities/anon-retrieval.entity.ts | 6 +- .../src/database/entities/retrieval.entity.ts | 2 +- .../1762000000000-CreateAnonRetrievals.ts | 1 + apps/backend/src/retrieval-anon/README.md | 155 ------------------ .../anon-piece-selector.service.ts | 3 +- .../retrieval-anon/anon-retrieval.service.ts | 1 + apps/backend/src/retrieval-anon/types.ts | 1 + 7 files changed, 11 insertions(+), 158 deletions(-) delete mode 100644 apps/backend/src/retrieval-anon/README.md diff --git a/apps/backend/src/database/entities/anon-retrieval.entity.ts b/apps/backend/src/database/entities/anon-retrieval.entity.ts index c28d0338..1c15e4ac 100644 --- a/apps/backend/src/database/entities/anon-retrieval.entity.ts +++ b/apps/backend/src/database/entities/anon-retrieval.entity.ts @@ -1,6 +1,6 @@ import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; -import { RetrievalStatus, ServiceType } from "../types.js"; import { BigIntColumn } from "../helpers/bigint-column.js"; +import { RetrievalStatus, ServiceType } from "../types.js"; /** * Anonymous retrieval check records — pieces the dealbot did NOT upload, @@ -31,6 +31,10 @@ export class AnonRetrieval { @BigIntColumn({ name: "piece_id" }) pieceId!: bigint; + /** Raw (unpadded) piece size in bytes, as reported by the subgraph at selection time. */ + @BigIntColumn({ name: "raw_size" }) + rawSize!: bigint; + @Column({ name: "with_ipfs_indexing", type: "boolean" }) withIpfsIndexing!: boolean; diff --git a/apps/backend/src/database/entities/retrieval.entity.ts b/apps/backend/src/database/entities/retrieval.entity.ts index 6e0ee5bb..18a59814 100644 --- a/apps/backend/src/database/entities/retrieval.entity.ts +++ b/apps/backend/src/database/entities/retrieval.entity.ts @@ -72,5 +72,5 @@ export class Retrieval { // Relations @ManyToOne("Deal", "retrievals", { onDelete: "CASCADE" }) @JoinColumn({ name: "deal_id" }) - deal: Deal; + deal: Deal | null; } diff --git a/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts b/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts index 3f682099..4925b04b 100644 --- a/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts +++ b/apps/backend/src/database/migrations/1762000000000-CreateAnonRetrievals.ts @@ -17,6 +17,7 @@ export class CreateAnonRetrievals1762000000000 implements MigrationInterface { piece_cid VARCHAR NOT NULL, data_set_id BIGINT NOT NULL, piece_id BIGINT NOT NULL, + raw_size BIGINT NOT NULL, with_ipfs_indexing BOOLEAN NOT NULL, ipfs_root_cid VARCHAR NULL, service_type VARCHAR NOT NULL DEFAULT 'direct_sp', diff --git a/apps/backend/src/retrieval-anon/README.md b/apps/backend/src/retrieval-anon/README.md deleted file mode 100644 index 7abf9f8c..00000000 --- a/apps/backend/src/retrieval-anon/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# Anonymous retrieval check - -The `retrievalAnon` check probes an SP using pieces the dealbot did **not** -upload itself, to detect SPs that serve the dealbot's own deals well but -perform poorly on arbitrary storage. See -[issue #427](https://github.com/FilOzone/dealbot/issues/427) for the full -motivation. - -This document describes the **piece selection** step. The subsequent piece -retrieval, CommP verification, CAR validation, IPNI lookup and `/ipfs` block -fetching follow the same shape as the basic retrieval check. - -## Goals - -1. **Uniform random** across the SP's entire active pool — not biased toward - recent writes, specific payers, or specific sizes. -2. **Prefer `withIPFSIndexing` pieces** so CAR/IPNI validation has something - meaningful to check, but still exercise non-indexed pieces so an SP can't - optimise only its CAR corpus. -3. **Cover a realistic spread of piece sizes** — big enough for useful - bandwidth measurements, not so big that SPs with only small deals are - skipped. -4. **Respect termination signals** — exclude datasets with `isActive: false`, - `fwssPayer == dealbot`, or `pdpPaymentEndEpoch <= currentEpoch`. -5. **Avoid immediate repeats** — don't retest a piece already tested in the - last 500 anonymous retrievals. - -## How it works - -Every `Root` in the subgraph carries a `sampleKey = keccak256(id)` populated -once when the root is indexed. Because keccak256 is uniform over 256 bits and -independent of creation order, dataset, and size, `sampleKey` sorts roots -into a uniform random permutation that is stable across queries. - -To draw a sample the backend: - -1. Picks a **size bucket** by weighted random: - - | bucket | size range (raw bytes) | weight | - |---|---|---| - | `small` | `[1, 64 MiB)` | 0.2 | - | `medium` | `[64 MiB, 1 GiB)` | 0.5 | - | `large` | `[1 GiB, 32 GiB]` | 0.3 | - -2. Picks the **pool**: `withIPFSIndexing: true` with probability 0.8; - otherwise no filter on `withIPFSIndexing` (both indexed and non-indexed - pieces are eligible). - -3. Generates 32 random bytes as `$sampleKey` and queries: - - ```graphql - roots( - first: 1 - orderBy: sampleKey - orderDirection: asc - where: { - sampleKey_gte: $sampleKey - removed: false - rawSize_gte: $minSize - rawSize_lte: $maxSize - proofSet_: { - fwssServiceProvider: $sp - fwssPayer_not: $dealbotPayer - isActive: true - withIPFSIndexing: true # only for the "indexed" pool query - } - } - ) { rootId cid rawSize ipfsRootCID proofSet { setId withIPFSIndexing fwssPayer pdpPaymentEndEpoch } } - ``` - - The result is the root with the smallest `sampleKey ≥ $sampleKey` that - satisfies the filters — a uniform random pick, in O(log N), with no - `skip` ceiling. - -4. Drops the pick if `pdpPaymentEndEpoch` has already passed the latest - indexed block, or if its CID appears in the last 500 anonymous - retrievals. On a drop, redraws once with a fresh `$sampleKey`. - -5. If no piece survives, falls back through this order: - - 1. Same bucket, opposite pool. - 2. Any bucket (`[0, 2^63-1]`), indexed pool. - 3. Any bucket, any pool. - - Each attempt uses a fresh `$sampleKey` and does up to two draws before - moving on. - -### Worked example - -An SP with 50k active FWSS pieces is up for a probe. - -1. Weighted random picks `medium`. Coin flip picks `indexed` pool. -2. `$sampleKey = 0x7fb3…c91e`. Subgraph returns the piece with the smallest - `sampleKey ≥ $sampleKey` whose raw size is in `[64 MiB, 1 GiB)`, whose - dataset is active, not paid by the dealbot, and marked - `withIPFSIndexing`. -3. Its `pdpPaymentEndEpoch` is null and its CID isn't in the last 500 anon - retrievals. Accepted. - -Total: one subgraph call. For an SP whose `medium/indexed` pool is empty -(small, non-CAR-heavy SP) the selector redraws once, tries `medium/any` -twice, and will land in `any/indexed` or `any/any` within a couple hundred -milliseconds. - -## Why a dedicated `sampleKey` field? - -GraphQL has no native random operator. The two obvious alternatives both -break at the scales FOC expects: - -- **`first: 1, skip: random(count)`** — The Graph caps `skip` at 5000. A - single mainnet SP can already exceed this. -- **Ordering by `id`** — `Root.id = "-"` is clustered by - dataset age (and lexicographically quirky: `"1-10" < "1-2"`), so random - `id_gte` picks skew heavily toward whichever `setId` prefix the random - hex lands on. - -A precomputed keccak hash sidesteps both: the sort is uniform, the lookup is -indexed (graph-node indexes every scalar automatically), and the field is -immutable — no maintenance cost on Root updates. - -## What this replaces - -The previous selector fetched the last 100 datasets × last 50 roots for an -SP, filtered out dealbot-owned and terminated ones client-side, and picked -from an in-memory pool with an "prefer IPFS-indexed" post-filter. That -implementation had three defects this design fixes: - -1. An SP with more than 100 datasets, or more than 50 roots per dataset, - was only ever probed on its newest corner of storage. -2. "Prefer IPFS-indexed" was applied after the pool was already truncated - to the 100×50 recent window — indexed pieces outside that window were - unreachable. -3. A cross-SP 500-piece dedup window was applied to a small per-SP pool, so - it could starve out quickly for busy SPs. - -The new selector also: - -- Enforces size bucketing (`small / medium / large`), addressing rvagg's - "big enough for bandwidth metrics, not so big as to exclude small SPs" - concern. -- Moves `isActive` and `fwssPayer_not: dealbot` into the subgraph `where:` - clause (one query round-trip instead of a client-side filter loop). -- Keeps `pdpPaymentEndEpoch` as a client-side check because GraphQL - nullable-BigInt comparison semantics would require multiple queries. - -## Tunables - -All in `anon-piece-selector.service.ts`: - -- `SIZE_BUCKETS` — bucket boundaries. -- `BUCKET_WEIGHTS` — bucket draw probabilities (must sum to 1). -- `IPFS_INDEXED_SAMPLE_RATE` — fraction of draws that start in the indexed - pool (default 0.8). -- `RECENT_DEDUP_WINDOW` — how many recent anon retrievals are excluded - (default 500). diff --git a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts index 2af6f167..acc19832 100644 --- a/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts +++ b/apps/backend/src/retrieval-anon/anon-piece-selector.service.ts @@ -63,7 +63,7 @@ export class AnonPieceSelectorService { /** * Select an anonymous piece to test against the given SP. * - * Strategy (see README.md for the full rationale): + * Strategy: * 1. Pick a size bucket by weighted random. * 2. Pick a pool (`indexed` 80% / `any` 20%). * 3. Generate a uniform-random sampleKey and query the subgraph for the @@ -114,6 +114,7 @@ export class AnonPieceSelectorService { serviceProvider: spAddress.toLowerCase(), withIPFSIndexing: piece.withIPFSIndexing, ipfsRootCid: piece.ipfsRootCid, + rawSize: piece.rawSize, }; } } diff --git a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts index a5b9b673..6ea50fb9 100644 --- a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts +++ b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts @@ -134,6 +134,7 @@ export class AnonRetrievalService { pieceCid: piece.pieceCid, dataSetId: BigInt(piece.dataSetId), pieceId: BigInt(piece.pieceId), + rawSize: BigInt(piece.rawSize), withIpfsIndexing: piece.withIPFSIndexing, ipfsRootCid: piece.ipfsRootCid, serviceType: ServiceType.DIRECT_SP, diff --git a/apps/backend/src/retrieval-anon/types.ts b/apps/backend/src/retrieval-anon/types.ts index 03c61712..a04137f5 100644 --- a/apps/backend/src/retrieval-anon/types.ts +++ b/apps/backend/src/retrieval-anon/types.ts @@ -6,6 +6,7 @@ export type AnonPiece = { serviceProvider: string; withIPFSIndexing: boolean; ipfsRootCid: string | null; + rawSize: string; }; /** Result of piece retrieval. */ From 3791f4c5cc521331a8471fbbe3440a543753a7de Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 14:06:48 +0200 Subject: [PATCH 16/19] add(retrieval-anon): job timeout configuration --- apps/backend/.env.example | 1 + apps/backend/src/config/app.config.ts | 10 ++++++++++ apps/backend/src/jobs/jobs.service.ts | 2 +- docs/environment-variables.md | 21 ++++++++++++++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index eb6552b1..c45aab37 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -64,6 +64,7 @@ JOB_SCHEDULE_PHASE_SECONDS=0 JOB_ENQUEUE_JITTER_SECONDS=0 DEAL_JOB_TIMEOUT_SECONDS=360 # 6m: Max runtime for deal jobs (TODO: reduce default to 3m) RETRIEVAL_JOB_TIMEOUT_SECONDS=60 # 1m: Max runtime for retrieval jobs (TODO: reduce default to 30s) +ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS=360 # 6m: Max runtime for anon retrieval jobs (pieces up to ~70 MiB) IPFS_BLOCK_FETCH_CONCURRENCY=6 # Parallel block fetches when validating IPFS DAGs DEALBOT_PGBOSS_POOL_MAX=1 DEALBOT_PGBOSS_SCHEDULER_ENABLED=true diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts index 7b11becd..69b0eecd 100644 --- a/apps/backend/src/config/app.config.ts +++ b/apps/backend/src/config/app.config.ts @@ -92,6 +92,7 @@ export const configValidationSchema = Joi.object({ JOB_ENQUEUE_JITTER_SECONDS: Joi.number().min(0).default(0), DEAL_JOB_TIMEOUT_SECONDS: Joi.number().min(120).default(360), // 6 minutes max runtime for data storage jobs (TODO: reduce default to 3 minutes) RETRIEVAL_JOB_TIMEOUT_SECONDS: Joi.number().min(60).default(60), // 1 minute max runtime for retrieval jobs (TODO: reduce default to 30 seconds) + ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS: Joi.number().min(60).default(360), // 6 minutes max runtime for anon retrieval jobs (pieces can be up to ~70 MiB) DATA_SET_CREATION_JOB_TIMEOUT_SECONDS: Joi.number().min(60).default(300), // 5 minutes max runtime for dataset creation jobs IPFS_BLOCK_FETCH_CONCURRENCY: Joi.number().integer().min(1).max(32).default(6), ANON_RETRIEVAL_BLOCK_SAMPLE_COUNT: Joi.number().integer().min(1).max(50).default(5), @@ -258,6 +259,14 @@ export interface IJobsConfig { * Uses AbortController to actively cancel job execution. */ retrievalJobTimeoutSeconds: number; + /** + * Maximum runtime (seconds) for anonymous retrieval jobs before forced abort. + * + * Anonymous retrievals fetch arbitrary pieces (up to ~70 MiB), so this is + * typically larger than `retrievalJobTimeoutSeconds`. Uses AbortController + * to actively cancel job execution while still persisting partial metrics. + */ + anonRetrievalJobTimeoutSeconds: number; /** * Target number of piece cleanup runs per storage provider per hour. * @@ -393,6 +402,7 @@ export function loadConfig(): IConfig { enqueueJitterSeconds: Number.parseInt(process.env.JOB_ENQUEUE_JITTER_SECONDS || "0", 10), dealJobTimeoutSeconds: Number.parseInt(process.env.DEAL_JOB_TIMEOUT_SECONDS || "360", 10), retrievalJobTimeoutSeconds: Number.parseInt(process.env.RETRIEVAL_JOB_TIMEOUT_SECONDS || "60", 10), + anonRetrievalJobTimeoutSeconds: Number.parseInt(process.env.ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS || "360", 10), retrievalsAnonPerSpPerHour: Number.parseFloat( process.env.RETRIEVALS_ANON_PER_SP_PER_HOUR || process.env.RETRIEVALS_PER_SP_PER_HOUR || "2", ), diff --git a/apps/backend/src/jobs/jobs.service.ts b/apps/backend/src/jobs/jobs.service.ts index 3e0505a5..d1316074 100644 --- a/apps/backend/src/jobs/jobs.service.ts +++ b/apps/backend/src/jobs/jobs.service.ts @@ -660,7 +660,7 @@ export class JobsService implements OnModuleInit, OnApplicationShutdown { // Create AbortController for job timeout enforcement const abortController = new AbortController(); - const timeoutSeconds = this.configService.get("jobs").retrievalJobTimeoutSeconds; + const timeoutSeconds = this.configService.get("jobs").anonRetrievalJobTimeoutSeconds; const timeoutMs = Math.max(60000, timeoutSeconds * 1000); const effectiveTimeoutSeconds = Math.round(timeoutMs / 1000); const abortReason = new Error(`Anon retrieval job timeout (${effectiveTimeoutSeconds}s) for ${spAddress}`); diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 0a76cd83..a09d879c 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -11,7 +11,7 @@ This document provides a comprehensive guide to all environment variables used b | [Blockchain](#blockchain-configuration) | `NETWORK`, `RPC_URL`, `WALLET_ADDRESS`, `WALLET_PRIVATE_KEY`, `SESSION_KEY_PRIVATE_KEY`, `CHECK_DATASET_CREATION_FEES`, `USE_ONLY_APPROVED_PROVIDERS`, `SUBGRAPH_ENDPOINT` | | [Dataset Versioning](#dataset-versioning) | `DEALBOT_DATASET_VERSION` | | [Scheduling](#scheduling-configuration) | `PROVIDERS_REFRESH_INTERVAL_SECONDS`, `DATA_RETENTION_POLL_INTERVAL_SECONDS`, `DEALBOT_MAINTENANCE_WINDOWS_UTC`, `DEALBOT_MAINTENANCE_WINDOW_MINUTES` | -| [Jobs (pg-boss)](#jobs-pg-boss) | `DEALBOT_PGBOSS_SCHEDULER_ENABLED`, `DEALBOT_PGBOSS_POOL_MAX`, `DEALS_PER_SP_PER_HOUR`, `DATASET_CREATIONS_PER_SP_PER_HOUR`, `RETRIEVALS_PER_SP_PER_HOUR`, `JOB_SCHEDULER_POLL_SECONDS`, `JOB_WORKER_POLL_SECONDS`, `PG_BOSS_LOCAL_CONCURRENCY`, `JOB_CATCHUP_MAX_ENQUEUE`, `JOB_SCHEDULE_PHASE_SECONDS`, `JOB_ENQUEUE_JITTER_SECONDS`, `DEAL_JOB_TIMEOUT_SECONDS`, `RETRIEVAL_JOB_TIMEOUT_SECONDS`, `IPFS_BLOCK_FETCH_CONCURRENCY` | +| [Jobs (pg-boss)](#jobs-pg-boss) | `DEALBOT_PGBOSS_SCHEDULER_ENABLED`, `DEALBOT_PGBOSS_POOL_MAX`, `DEALS_PER_SP_PER_HOUR`, `DATASET_CREATIONS_PER_SP_PER_HOUR`, `RETRIEVALS_PER_SP_PER_HOUR`, `JOB_SCHEDULER_POLL_SECONDS`, `JOB_WORKER_POLL_SECONDS`, `PG_BOSS_LOCAL_CONCURRENCY`, `JOB_CATCHUP_MAX_ENQUEUE`, `JOB_SCHEDULE_PHASE_SECONDS`, `JOB_ENQUEUE_JITTER_SECONDS`, `DEAL_JOB_TIMEOUT_SECONDS`, `RETRIEVAL_JOB_TIMEOUT_SECONDS`, `ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS`, `IPFS_BLOCK_FETCH_CONCURRENCY` | | [Dataset](#dataset-configuration) | `DEALBOT_LOCAL_DATASETS_PATH`, `RANDOM_PIECE_SIZES` | | [Timeouts](#timeout-configuration) | `CONNECT_TIMEOUT_MS`, `HTTP_REQUEST_TIMEOUT_MS`, `HTTP2_REQUEST_TIMEOUT_MS`, `IPNI_VERIFICATION_TIMEOUT_MS`, `IPNI_VERIFICATION_POLLING_MS` | | [Piece Cleanup](#piece-cleanup) | `MAX_DATASET_STORAGE_SIZE_BYTES`, `TARGET_DATASET_STORAGE_SIZE_BYTES`, `JOB_PIECE_CLEANUP_PER_SP_PER_HOUR`, `MAX_PIECE_CLEANUP_RUNTIME_SECONDS` | @@ -786,6 +786,25 @@ Use this to stagger multiple dealbot deployments that are not sharing a database **Note**: This is independent of HTTP-level timeouts. The job timeout enforces end-to-end execution time of a Retrieval Check job. +--- + +### `ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS` + +- **Type**: `number` +- **Required**: No +- **Default**: `360` (6 minutes) +- **Minimum**: `60` +- **Enforced**: Yes (config validation) + +**Role**: Maximum runtime for anonymous retrieval jobs before forced abort. Anonymous retrievals fetch arbitrary pieces (up to ~70 MiB) that were not produced by the dealbot, so this is typically larger than `RETRIEVAL_JOB_TIMEOUT_SECONDS`. When the timeout trips, partial metrics (`ttfb_ms`, `bytes_retrieved`, `response_code`) are still persisted so the abort is not silently lost. + +**When to update**: + +- Increase if large pieces are consistently being cut off mid-download +- Decrease to detect and fail stuck retrievals faster + +**Note**: This is independent of HTTP-level timeouts (`CONNECT_TIMEOUT_MS`, `HTTP2_REQUEST_TIMEOUT_MS`). The job timeout covers the end-to-end execution of an Anon Retrieval Check (piece selection, download, CommP validation, CAR/IPNI validation). + --- ### `IPFS_BLOCK_FETCH_CONCURRENCY` From 8890c70ce5279d29b17b54ecf318d3595b017f75 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 14:42:53 +0200 Subject: [PATCH 17/19] fix(retrieval-anon): track partial retrieval data --- apps/backend/.env.example | 10 +- apps/backend/.tool-versions | 2 + apps/backend/src/config/app.config.ts | 56 +++++- .../http-client/http-client.service.spec.ts | 48 +++++ .../src/http-client/http-client.service.ts | 49 ++++- apps/backend/src/http-client/types.ts | 2 + .../anon-retrieval.service.spec.ts | 189 ++++++++++++++++++ .../retrieval-anon/anon-retrieval.service.ts | 167 +++++++++++----- .../retrieval-anon/piece-retrieval.service.ts | 34 +++- apps/backend/src/retrieval-anon/types.ts | 1 + 10 files changed, 489 insertions(+), 69 deletions(-) create mode 100644 apps/backend/.tool-versions create mode 100644 apps/backend/src/retrieval-anon/anon-retrieval.service.spec.ts diff --git a/apps/backend/.env.example b/apps/backend/.env.example index c45aab37..26469c52 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -78,9 +78,13 @@ PROXY_LIST=http://username:password@host:port,http://username:password@host:port PROXY_LOCATIONS=l1,l2 # Timeout Configuration (in milliseconds) -CONNECT_TIMEOUT_MS=10000 # 10s: Initial connection timeout -HTTP_REQUEST_TIMEOUT_MS=240000 # 4m: Total transfer timeout for HTTP/1.1 (10MiB @ 170KB/s + overhead) -HTTP2_REQUEST_TIMEOUT_MS=240000 # 4m: Total transfer timeout for HTTP/2 (10MiB @ 170KB/s + overhead) +CONNECT_TIMEOUT_MS=10000 # 10s: Connection + response-headers timeout (scoped to the header phase only) +# HTTP_REQUEST_TIMEOUT_MS and HTTP2_REQUEST_TIMEOUT_MS default to the longest job timeout above +# (max of DEAL_/RETRIEVAL_/ANON_RETRIEVAL_/DATA_SET_CREATION_/MAX_PIECE_CLEANUP_ * 1000 ms) so the +# HTTP-level ceiling never pre-empts a job-scoped AbortSignal. Only override when you have a non-job +# caller of HttpClientService that needs a specific deadline. +# HTTP_REQUEST_TIMEOUT_MS=360000 +# HTTP2_REQUEST_TIMEOUT_MS=360000 # SP Blocklists configuration # BLOCKED_SP_IDS=1234,5678 diff --git a/apps/backend/.tool-versions b/apps/backend/.tool-versions new file mode 100644 index 00000000..7352e387 --- /dev/null +++ b/apps/backend/.tool-versions @@ -0,0 +1,2 @@ +nodejs 25.8.1 +pnpm 10.33.0 diff --git a/apps/backend/src/config/app.config.ts b/apps/backend/src/config/app.config.ts index 69b0eecd..255a6217 100644 --- a/apps/backend/src/config/app.config.ts +++ b/apps/backend/src/config/app.config.ts @@ -127,8 +127,9 @@ export const configValidationSchema = Joi.object({ // Timeouts (in milliseconds) CONNECT_TIMEOUT_MS: Joi.number().min(1000).default(10000), // 10 seconds to establish connection/receive headers - HTTP_REQUEST_TIMEOUT_MS: Joi.number().min(1000).default(240000), // 4 minutes total for HTTP requests (10MiB @ 170KB/s + overhead) - HTTP2_REQUEST_TIMEOUT_MS: Joi.number().min(1000).default(240000), // 4 minutes total for HTTP/2 requests (10MiB @ 170KB/s + overhead) + // Defaults intentionally omitted so loadConfig can derive them from the longest job timeout. + HTTP_REQUEST_TIMEOUT_MS: Joi.number().min(1000).optional(), + HTTP2_REQUEST_TIMEOUT_MS: Joi.number().min(1000).optional(), IPNI_VERIFICATION_TIMEOUT_MS: Joi.number().min(1000).default(60000), // 60 seconds max time to wait for IPNI verification IPNI_VERIFICATION_POLLING_MS: Joi.number().min(250).default(2000), // 2 seconds between IPNI verification polls @@ -336,6 +337,43 @@ export interface IConfig { } export function loadConfig(): IConfig { + const jobTimeoutSeconds = { + deal: Number.parseInt(process.env.DEAL_JOB_TIMEOUT_SECONDS || "360", 10), + retrieval: Number.parseInt(process.env.RETRIEVAL_JOB_TIMEOUT_SECONDS || "60", 10), + anonRetrieval: Number.parseInt(process.env.ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS || "360", 10), + dataSetCreation: Number.parseInt(process.env.DATA_SET_CREATION_JOB_TIMEOUT_SECONDS || "300", 10), + pieceCleanup: Number.parseInt(process.env.MAX_PIECE_CLEANUP_RUNTIME_SECONDS || "300", 10), + }; + + // HTTP-level request timeouts default to the longest job timeout so the + // per-request ceiling never caps below the per-job budget. Any job-scoped + // AbortSignal fires first and is authoritative; the HTTP timer only kicks + // in for callers that do not pass a parent signal. + const longestJobTimeoutMs = Math.max(...Object.values(jobTimeoutSeconds)) * 1000; + + const httpRequestTimeoutMs = Number.parseInt(process.env.HTTP_REQUEST_TIMEOUT_MS || String(longestJobTimeoutMs), 10); + const http2RequestTimeoutMs = Number.parseInt( + process.env.HTTP2_REQUEST_TIMEOUT_MS || String(longestJobTimeoutMs), + 10, + ); + + // Misconfiguration guard: if someone explicitly sets an HTTP timeout below + // the longest job timeout, the HTTP-level timer will abort in-flight work + // before the job signal has a chance to report it. Warn loudly so this is + // caught at boot rather than inferred from short-timeout incidents later. + for (const [name, value] of [ + ["HTTP_REQUEST_TIMEOUT_MS", httpRequestTimeoutMs], + ["HTTP2_REQUEST_TIMEOUT_MS", http2RequestTimeoutMs], + ] as const) { + if (value < longestJobTimeoutMs) { + // eslint-disable-next-line no-console + console.warn( + `[config] ${name}=${value}ms is lower than the longest job timeout (${longestJobTimeoutMs}ms). ` + + `HTTP requests may abort before the job signal fires, producing short, unexplained timeouts.`, + ); + } + } + return { app: { env: process.env.NODE_ENV || "development", @@ -400,15 +438,15 @@ export function loadConfig(): IConfig { catchupMaxEnqueue: Number.parseInt(process.env.JOB_CATCHUP_MAX_ENQUEUE || "10", 10), schedulePhaseSeconds: Number.parseInt(process.env.JOB_SCHEDULE_PHASE_SECONDS || "0", 10), enqueueJitterSeconds: Number.parseInt(process.env.JOB_ENQUEUE_JITTER_SECONDS || "0", 10), - dealJobTimeoutSeconds: Number.parseInt(process.env.DEAL_JOB_TIMEOUT_SECONDS || "360", 10), - retrievalJobTimeoutSeconds: Number.parseInt(process.env.RETRIEVAL_JOB_TIMEOUT_SECONDS || "60", 10), - anonRetrievalJobTimeoutSeconds: Number.parseInt(process.env.ANON_RETRIEVAL_JOB_TIMEOUT_SECONDS || "360", 10), + dealJobTimeoutSeconds: jobTimeoutSeconds.deal, + retrievalJobTimeoutSeconds: jobTimeoutSeconds.retrieval, + anonRetrievalJobTimeoutSeconds: jobTimeoutSeconds.anonRetrieval, retrievalsAnonPerSpPerHour: Number.parseFloat( process.env.RETRIEVALS_ANON_PER_SP_PER_HOUR || process.env.RETRIEVALS_PER_SP_PER_HOUR || "2", ), - dataSetCreationJobTimeoutSeconds: Number.parseInt(process.env.DATA_SET_CREATION_JOB_TIMEOUT_SECONDS || "300", 10), + dataSetCreationJobTimeoutSeconds: jobTimeoutSeconds.dataSetCreation, pieceCleanupPerSpPerHour: Number.parseFloat(process.env.JOB_PIECE_CLEANUP_PER_SP_PER_HOUR || String(1 / 24)), - maxPieceCleanupRuntimeSeconds: Number.parseInt(process.env.MAX_PIECE_CLEANUP_RUNTIME_SECONDS || "300", 10), + maxPieceCleanupRuntimeSeconds: jobTimeoutSeconds.pieceCleanup, }, dataset: { localDatasetsPath: process.env.DEALBOT_LOCAL_DATASETS_PATH || DEFAULT_LOCAL_DATASETS_PATH, @@ -430,8 +468,8 @@ export function loadConfig(): IConfig { }, timeouts: { connectTimeoutMs: Number.parseInt(process.env.CONNECT_TIMEOUT_MS || "10000", 10), - httpRequestTimeoutMs: Number.parseInt(process.env.HTTP_REQUEST_TIMEOUT_MS || "240000", 10), - http2RequestTimeoutMs: Number.parseInt(process.env.HTTP2_REQUEST_TIMEOUT_MS || "240000", 10), + httpRequestTimeoutMs, + http2RequestTimeoutMs, ipniVerificationTimeoutMs: Number.parseInt(process.env.IPNI_VERIFICATION_TIMEOUT_MS || "60000", 10), ipniVerificationPollingMs: Number.parseInt(process.env.IPNI_VERIFICATION_POLLING_MS || "2000", 10), }, diff --git a/apps/backend/src/http-client/http-client.service.spec.ts b/apps/backend/src/http-client/http-client.service.spec.ts index 96604139..b3856df5 100644 --- a/apps/backend/src/http-client/http-client.service.spec.ts +++ b/apps/backend/src/http-client/http-client.service.spec.ts @@ -85,4 +85,52 @@ describe("HttpClientService", () => { await assertion; }); + + it("returns partial bytes and metrics when HTTP/2 download is aborted after headers", async () => { + const service = await createService(); + + const parentAbort = new AbortController(); + + async function* abortingBody() { + yield Buffer.from("hello"); + yield Buffer.from(" world"); + // Simulate an abort mid-stream after two chunks. + parentAbort.abort(new Error("Anon retrieval job timeout (60s) for sp1")); + throw new Error("aborted"); + } + + undiciRequestMock.mockImplementationOnce(async () => ({ + statusCode: 200, + body: abortingBody(), + })); + + const result = await service.requestWithMetrics("http://example.com/piece", { + httpVersion: "2", + signal: parentAbort.signal, + }); + + expect(result.aborted).toBe(true); + expect(result.abortReason).toContain("timeout"); + expect(result.metrics.statusCode).toBe(200); + expect(result.metrics.responseSize).toBe(11); + expect(Buffer.isBuffer(result.data) ? result.data.toString() : "").toBe("hello world"); + }); + + it("rethrows non-abort download errors on HTTP/2", async () => { + const service = await createService(); + + async function* brokenBody() { + yield Buffer.from("partial"); + throw new Error("network reset"); + } + + undiciRequestMock.mockImplementationOnce(async () => ({ + statusCode: 200, + body: brokenBody(), + })); + + await expect(service.requestWithMetrics("http://example.com/piece", { httpVersion: "2" })).rejects.toThrow( + "network reset", + ); + }); }); diff --git a/apps/backend/src/http-client/http-client.service.ts b/apps/backend/src/http-client/http-client.service.ts index 48e10e5c..cf845177 100644 --- a/apps/backend/src/http-client/http-client.service.ts +++ b/apps/backend/src/http-client/http-client.service.ts @@ -115,8 +115,15 @@ export class HttpClientService { statusCode = response.statusCode; const chunks: Buffer[] = []; - for await (const chunk of response.body) { - chunks.push(Buffer.from(chunk)); + let downloadError: unknown; + try { + for await (const chunk of response.body) { + chunks.push(Buffer.from(chunk)); + } + } catch (error) { + // Download-phase failures (e.g. abort signal) fall through so we can + // return the partial buffer + metrics collected so far. + downloadError = error; } const dataBuffer = Buffer.concat(chunks); @@ -133,6 +140,29 @@ export class HttpClientService { httpVersion: "2", }; + if (downloadError !== undefined) { + const aborted = options.signal?.aborted === true || isAbortLikeError(downloadError); + if (!aborted) { + throw downloadError; + } + const abortReason = describeAbortReason(options.signal, downloadError); + this.logger.warn({ + event: "http2_download_aborted", + message: "HTTP/2 download aborted after headers; returning partial data", + url, + bytesReceived: dataBuffer.length, + totalTime: metrics.totalTime, + ttfb: metrics.ttfb, + abortReason, + }); + return { + data: dataBuffer as T, + metrics, + aborted: true, + abortReason, + }; + } + return { data: dataBuffer as T, metrics, @@ -276,3 +306,18 @@ export class HttpClientService { }; } } + +function isAbortLikeError(error: unknown): boolean { + if (error instanceof Error) { + return error.name === "AbortError" || error.name === "TimeoutError" || /abort/i.test(error.message); + } + return false; +} + +function describeAbortReason(signal: AbortSignal | undefined, fallback: unknown): string { + const reason = signal?.reason; + if (reason instanceof Error && reason.message) return reason.message; + if (typeof reason === "string" && reason.length > 0) return reason; + if (fallback instanceof Error && fallback.message) return fallback.message; + return "aborted"; +} diff --git a/apps/backend/src/http-client/types.ts b/apps/backend/src/http-client/types.ts index 7e48ce7d..26892ee6 100644 --- a/apps/backend/src/http-client/types.ts +++ b/apps/backend/src/http-client/types.ts @@ -13,4 +13,6 @@ export interface RequestMetrics { export interface RequestWithMetrics { data: T; metrics: RequestMetrics; + aborted?: boolean; // Set when the request was aborted mid-download after response headers arrived. + abortReason?: string; // Error message when `aborted` is true; human-readable summary of the abort reason. } diff --git a/apps/backend/src/retrieval-anon/anon-retrieval.service.spec.ts b/apps/backend/src/retrieval-anon/anon-retrieval.service.spec.ts new file mode 100644 index 00000000..61e97105 --- /dev/null +++ b/apps/backend/src/retrieval-anon/anon-retrieval.service.spec.ts @@ -0,0 +1,189 @@ +import type { Repository } from "typeorm"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { AnonRetrieval } from "../database/entities/anon-retrieval.entity.js"; +import type { StorageProvider } from "../database/entities/storage-provider.entity.js"; +import { RetrievalStatus } from "../database/types.js"; +import type { AnonRetrievalCheckMetrics } from "../metrics-prometheus/check-metrics.service.js"; +import type { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; +import type { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; +import { AnonRetrievalService } from "./anon-retrieval.service.js"; +import type { CarValidationService } from "./car-validation.service.js"; +import type { PieceRetrievalService } from "./piece-retrieval.service.js"; +import type { PieceRetrievalResult } from "./types.js"; + +const SP_ADDRESS = "0xaaaa0000000000000000000000000000000000aa"; + +const PIECE = { + pieceCid: "baga6ea4seaqpiece", + pieceId: "1", + dataSetId: "42", + rawSize: "1048576", + withIPFSIndexing: false, + ipfsRootCid: null, + serviceProvider: SP_ADDRESS, +}; + +function makeProvider(): StorageProvider { + return { + address: SP_ADDRESS, + providerId: 7, + name: "sp-test", + isApproved: true, + } as unknown as StorageProvider; +} + +function makeService(opts: { + pieceResult: PieceRetrievalResult; + fetchPieceImpl?: (signal?: AbortSignal) => Promise; +}): { + service: AnonRetrievalService; + saveSpy: ReturnType; + fetchSpy: ReturnType; +} { + const saveSpy = vi.fn(async (entity: AnonRetrieval) => entity); + const createdEntities: Partial[] = []; + const anonRetrievalRepository = { + create: vi.fn((data: Partial) => { + createdEntities.push(data); + return data; + }), + save: saveSpy, + } as unknown as Repository; + + const spRepository = { + findOne: vi.fn(async () => makeProvider()), + } as unknown as Repository; + + const anonPieceSelector = { + selectPieceForProvider: vi.fn(async () => PIECE), + } as unknown as AnonPieceSelectorService; + + const fetchSpy = vi.fn(opts.fetchPieceImpl ?? (async () => opts.pieceResult)); + const pieceRetrievalService = { + fetchPiece: fetchSpy, + } as unknown as PieceRetrievalService; + + const carValidationService = { + validateCarPiece: vi.fn(), + } as unknown as CarValidationService; + + const walletSdkService = { + getProviderInfo: vi.fn(() => ({ pdp: { serviceURL: "https://sp.test/" } })), + } as unknown as WalletSdkService; + + const metrics = { + observeFirstByteMs: vi.fn(), + observeLastByteMs: vi.fn(), + observeThroughput: vi.fn(), + observeCheckDuration: vi.fn(), + recordStatus: vi.fn(), + recordHttpResponseCode: vi.fn(), + recordCarParseStatus: vi.fn(), + recordIpniStatus: vi.fn(), + recordBlockFetchStatus: vi.fn(), + } as unknown as AnonRetrievalCheckMetrics; + + const service = new AnonRetrievalService( + anonPieceSelector, + pieceRetrievalService, + carValidationService, + walletSdkService, + metrics, + anonRetrievalRepository, + spRepository, + ); + + return { service, saveSpy, fetchSpy }; +} + +describe("AnonRetrievalService", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("persists partial metrics when fetchPiece returns aborted=true", async () => { + const partial: PieceRetrievalResult = { + success: false, + pieceCid: PIECE.pieceCid, + bytesReceived: 524288, + pieceBytes: null, + latencyMs: 42000, + ttfbMs: 150, + throughputBps: 12500, + statusCode: 200, + commPValid: false, + errorMessage: "Anon retrieval job timeout (60s) for sp1", + aborted: true, + }; + + const { service, saveSpy } = makeService({ pieceResult: partial }); + + await service.performForProvider(SP_ADDRESS); + + expect(saveSpy).toHaveBeenCalledTimes(1); + const saved = saveSpy.mock.calls[0][0] as Partial; + expect(saved.status).toBe(RetrievalStatus.FAILED); + expect(saved.bytesRetrieved).toBe(524288); + expect(saved.ttfbMs).toBe(150); + expect(saved.latencyMs).toBe(42000); + expect(saved.throughputBps).toBe(12500); + expect(saved.responseCode).toBe(200); + expect(saved.errorMessage).toContain("Anon retrieval job timeout"); + }); + + it("still saves a row when the signal aborts before fetchPiece runs", async () => { + const ac = new AbortController(); + ac.abort(new Error("Anon retrieval job timeout (60s) for sp1")); + + const never: PieceRetrievalResult = { + success: false, + pieceCid: PIECE.pieceCid, + bytesReceived: 0, + pieceBytes: null, + latencyMs: 0, + ttfbMs: 0, + throughputBps: 0, + statusCode: 0, + commPValid: false, + }; + + const { service, saveSpy, fetchSpy } = makeService({ pieceResult: never }); + + await service.performForProvider(SP_ADDRESS, ac.signal); + + expect(fetchSpy).not.toHaveBeenCalled(); + expect(saveSpy).toHaveBeenCalledTimes(1); + const saved = saveSpy.mock.calls[0][0] as Partial; + expect(saved.status).toBe(RetrievalStatus.FAILED); + expect(saved.errorMessage).toContain("Anon retrieval job timeout"); + expect(saved.bytesRetrieved).toBeNull(); + expect(saved.ttfbMs).toBeNull(); + }); + + it("still saves a row when fetchPiece throws unexpectedly", async () => { + const never: PieceRetrievalResult = { + success: false, + pieceCid: PIECE.pieceCid, + bytesReceived: 0, + pieceBytes: null, + latencyMs: 0, + ttfbMs: 0, + throughputBps: 0, + statusCode: 0, + commPValid: false, + }; + + const { service, saveSpy } = makeService({ + pieceResult: never, + fetchPieceImpl: async () => { + throw new Error("network down"); + }, + }); + + await expect(service.performForProvider(SP_ADDRESS)).rejects.toThrow("network down"); + + expect(saveSpy).toHaveBeenCalledTimes(1); + const saved = saveSpy.mock.calls[0][0] as Partial; + expect(saved.status).toBe(RetrievalStatus.FAILED); + }); +}); diff --git a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts index 6ea50fb9..d40fe315 100644 --- a/apps/backend/src/retrieval-anon/anon-retrieval.service.ts +++ b/apps/backend/src/retrieval-anon/anon-retrieval.service.ts @@ -11,7 +11,7 @@ import { WalletSdkService } from "../wallet-sdk/wallet-sdk.service.js"; import { AnonPieceSelectorService } from "./anon-piece-selector.service.js"; import { CarValidationService } from "./car-validation.service.js"; import { PieceRetrievalService } from "./piece-retrieval.service.js"; -import type { CarValidationResult } from "./types.js"; +import type { CarValidationResult, PieceRetrievalResult } from "./types.js"; @Injectable() export class AnonRetrievalService { @@ -70,65 +70,104 @@ export class AnonRetrievalService { const checkStart = Date.now(); const startedAt = new Date(); - // 2. Fetch the piece - signal?.throwIfAborted(); - const pieceResult = await this.pieceRetrievalService.fetchPiece(spAddress, piece.pieceCid, signal); + let pieceResult: PieceRetrievalResult | null = null; + let carResult: CarValidationResult | null = null; + let saved: AnonRetrieval | null = null; - // Emit piece retrieval metrics - this.metrics.observeFirstByteMs(labels, pieceResult.ttfbMs); - this.metrics.observeLastByteMs(labels, pieceResult.latencyMs); - this.metrics.observeThroughput(labels, pieceResult.throughputBps); - this.metrics.recordHttpResponseCode(labels, pieceResult.statusCode); + try { + // 2. Fetch the piece. fetchPiece never throws on abort — it returns a + // result with partial metrics so we can persist what we have. + if (signal?.aborted) { + pieceResult = buildAbortedPlaceholder(piece.pieceCid, signal.reason); + } else { + pieceResult = await this.pieceRetrievalService.fetchPiece(spAddress, piece.pieceCid, signal); + } - // 3. CAR validation (only if piece was successfully retrieved and has IPFS indexing) - let carResult: CarValidationResult | null = null; - if (pieceResult.success && piece.withIPFSIndexing && piece.ipfsRootCid && pieceResult.pieceBytes && provider) { - signal?.throwIfAborted(); - try { - carResult = await this.carValidationService.validateCarPiece( - pieceResult.pieceBytes, - provider, - piece.ipfsRootCid, - signal, + // Emit piece retrieval metrics + this.metrics.observeFirstByteMs(labels, pieceResult.ttfbMs); + this.metrics.observeLastByteMs(labels, pieceResult.latencyMs); + this.metrics.observeThroughput(labels, pieceResult.throughputBps); + this.metrics.recordHttpResponseCode(labels, pieceResult.statusCode); + + // 3. CAR validation (only if piece was successfully retrieved and has IPFS indexing) + if ( + pieceResult.success && + piece.withIPFSIndexing && + piece.ipfsRootCid && + pieceResult.pieceBytes && + provider && + !signal?.aborted + ) { + try { + carResult = await this.carValidationService.validateCarPiece( + pieceResult.pieceBytes, + provider, + piece.ipfsRootCid, + signal, + ); + } catch (error) { + this.logger.warn({ + ...logContext, + event: "anon_retrieval_car_validation_failed", + message: "CAR validation threw an error", + pieceCid: piece.pieceCid, + spAddress, + error: toStructuredError(error), + }); + } + } + + // Emit CAR validation metrics + if (carResult) { + this.metrics.recordCarParseStatus(labels, carResult.carParseable); + this.metrics.recordIpniStatus( + labels, + carResult.ipniValid === null ? "skipped" : carResult.ipniValid ? "valid" : "invalid", + ); + this.metrics.recordBlockFetchStatus( + labels, + carResult.blockFetchValid === null ? "skipped" : carResult.blockFetchValid ? "valid" : "invalid", ); - } catch (error) { - this.logger.warn({ - ...logContext, - event: "anon_retrieval_car_validation_failed", - message: "CAR validation threw an error", - pieceCid: piece.pieceCid, - spAddress, - error: toStructuredError(error), - }); + } else if (!pieceResult.success) { + // Piece retrieval failed — IPNI and block fetch were skipped + this.metrics.recordIpniStatus(labels, "skipped"); + this.metrics.recordBlockFetchStatus(labels, "skipped"); } - } - // Emit CAR validation metrics - if (carResult) { - this.metrics.recordCarParseStatus(labels, carResult.carParseable); - this.metrics.recordIpniStatus( - labels, - carResult.ipniValid === null ? "skipped" : carResult.ipniValid ? "valid" : "invalid", - ); - this.metrics.recordBlockFetchStatus( + // Overall check duration and status + this.metrics.observeCheckDuration(labels, Date.now() - checkStart); + this.metrics.recordStatus( labels, - carResult.blockFetchValid === null ? "skipped" : carResult.blockFetchValid ? "valid" : "invalid", + pieceResult.success ? "success" : pieceResult.aborted ? "failure.aborted" : "failure.http", ); - } else if (!pieceResult.success) { - // Piece retrieval failed — IPNI and block fetch were skipped - this.metrics.recordIpniStatus(labels, "skipped"); - this.metrics.recordBlockFetchStatus(labels, "skipped"); + } finally { + // Always save a record — even on abort or unexpected error — so we never + // lose the evidence (ttfb, bytes, response code) we already collected. + pieceResult ??= buildAbortedPlaceholder(piece.pieceCid, signal?.reason); + saved = await this.saveRetrievalRecord(spAddress, piece, pieceResult, carResult, startedAt, logContext); } - // Overall check duration and status - this.metrics.observeCheckDuration(labels, Date.now() - checkStart); - this.metrics.recordStatus(labels, pieceResult.success ? "success" : "failure.http"); + return saved; + } - // 4. Build the SP base URL for the retrieval endpoint + private async saveRetrievalRecord( + spAddress: string, + piece: { + pieceCid: string; + dataSetId: string; + pieceId: string; + rawSize: string; + withIPFSIndexing: boolean; + ipfsRootCid: string | null; + }, + pieceResult: PieceRetrievalResult, + carResult: CarValidationResult | null, + startedAt: Date, + logContext?: ProviderJobContext, + ): Promise { const providerInfo = this.walletSdkService.getProviderInfo(spAddress); const spBaseUrl = providerInfo?.pdp.serviceURL.replace(/\/$/, "") ?? spAddress; - // 5. Save retrieval record const retrieval = this.anonRetrievalRepository.create({ spAddress, pieceCid: piece.pieceCid, @@ -142,12 +181,12 @@ export class AnonRetrievalService { status: pieceResult.success ? RetrievalStatus.SUCCESS : RetrievalStatus.FAILED, startedAt, completedAt: new Date(), - latencyMs: Math.round(pieceResult.latencyMs), - ttfbMs: Math.round(pieceResult.ttfbMs), - throughputBps: Math.round(pieceResult.throughputBps), - bytesRetrieved: pieceResult.bytesReceived, - responseCode: pieceResult.statusCode, - errorMessage: pieceResult.errorMessage, + latencyMs: pieceResult.latencyMs > 0 ? Math.round(pieceResult.latencyMs) : null, + ttfbMs: pieceResult.ttfbMs > 0 ? Math.round(pieceResult.ttfbMs) : null, + throughputBps: pieceResult.throughputBps > 0 ? Math.round(pieceResult.throughputBps) : null, + bytesRetrieved: pieceResult.bytesReceived > 0 ? pieceResult.bytesReceived : null, + responseCode: pieceResult.statusCode > 0 ? pieceResult.statusCode : null, + errorMessage: pieceResult.errorMessage ?? null, commpValid: pieceResult.success ? pieceResult.commPValid : null, carValid: carResult ? carResult.ipniValid !== false && carResult.blockFetchValid !== false : null, }); @@ -163,6 +202,7 @@ export class AnonRetrievalService { spAddress, error: toStructuredError(error), }); + return null; } this.logger.log({ @@ -172,7 +212,10 @@ export class AnonRetrievalService { pieceCid: piece.pieceCid, spAddress, success: pieceResult.success, + aborted: pieceResult.aborted === true, latencyMs: pieceResult.latencyMs, + ttfbMs: pieceResult.ttfbMs, + bytesRetrieved: pieceResult.bytesReceived, carParseable: carResult?.carParseable, ipniValid: carResult?.ipniValid, blockFetchValid: carResult?.blockFetchValid, @@ -181,3 +224,21 @@ export class AnonRetrievalService { return retrieval; } } + +function buildAbortedPlaceholder(pieceCid: string, reason: unknown): PieceRetrievalResult { + const message = + reason instanceof Error && reason.message ? reason.message : typeof reason === "string" ? reason : "aborted"; + return { + success: false, + pieceCid, + bytesReceived: 0, + pieceBytes: null, + latencyMs: 0, + ttfbMs: 0, + throughputBps: 0, + statusCode: 0, + commPValid: false, + errorMessage: message, + aborted: true, + }; +} diff --git a/apps/backend/src/retrieval-anon/piece-retrieval.service.ts b/apps/backend/src/retrieval-anon/piece-retrieval.service.ts index 851f68ec..51150661 100644 --- a/apps/backend/src/retrieval-anon/piece-retrieval.service.ts +++ b/apps/backend/src/retrieval-anon/piece-retrieval.service.ts @@ -50,6 +50,34 @@ export class PieceRetrievalService { const { metrics } = result; const isSuccess = metrics.statusCode >= 200 && metrics.statusCode < 300; + const throughputBps = metrics.totalTime > 0 ? metrics.responseSize / (metrics.totalTime / 1000) : 0; + + if (result.aborted) { + this.logger.warn({ + event: "piece_fetch_aborted", + message: "Piece fetch aborted mid-download; returning partial metrics", + url, + pieceCid, + spAddress, + bytesReceived: metrics.responseSize, + ttfbMs: metrics.ttfb, + abortReason: result.abortReason, + }); + + return { + success: false, + pieceCid, + bytesReceived: metrics.responseSize, + pieceBytes: null, + latencyMs: metrics.totalTime, + ttfbMs: metrics.ttfb, + throughputBps, + statusCode: metrics.statusCode, + commPValid: false, + errorMessage: result.abortReason ?? "aborted", + aborted: true, + }; + } if (!isSuccess) { this.logger.warn({ @@ -68,7 +96,7 @@ export class PieceRetrievalService { pieceBytes: null, latencyMs: metrics.totalTime, ttfbMs: metrics.ttfb, - throughputBps: metrics.totalTime > 0 ? metrics.responseSize / (metrics.totalTime / 1000) : 0, + throughputBps, statusCode: metrics.statusCode, commPValid: false, errorMessage: `HTTP ${metrics.statusCode}`, @@ -77,7 +105,6 @@ export class PieceRetrievalService { const pieceBytes = Buffer.isBuffer(result.data) ? result.data : Buffer.from(result.data); const commPValid = await this.validateCommP(pieceBytes, pieceCid); - const throughputBps = metrics.totalTime > 0 ? metrics.responseSize / (metrics.totalTime / 1000) : 0; this.logger.debug({ event: "piece_fetch_success", @@ -101,12 +128,14 @@ export class PieceRetrievalService { commPValid, }; } catch (error) { + const aborted = signal?.aborted === true; this.logger.warn({ event: "piece_fetch_failed", message: "Piece fetch threw an error", url, pieceCid, spAddress, + aborted, error: toStructuredError(error), }); @@ -121,6 +150,7 @@ export class PieceRetrievalService { statusCode: 0, commPValid: false, errorMessage: error instanceof Error ? error.message : String(error), + aborted, }; } } diff --git a/apps/backend/src/retrieval-anon/types.ts b/apps/backend/src/retrieval-anon/types.ts index a04137f5..2c3384d5 100644 --- a/apps/backend/src/retrieval-anon/types.ts +++ b/apps/backend/src/retrieval-anon/types.ts @@ -21,6 +21,7 @@ export type PieceRetrievalResult = { statusCode: number; commPValid: boolean; errorMessage?: string; + aborted?: boolean; }; /** Result of CAR validation. */ From 06320f7a17d0fd5085e46be402d895b1df7b21f2 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 14:51:51 +0200 Subject: [PATCH 18/19] fix: don't fold connect and transfer timeout signals --- .../http-client/http-client.service.spec.ts | 49 +++++++++++++------ .../src/http-client/http-client.service.ts | 44 +++++++---------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/apps/backend/src/http-client/http-client.service.spec.ts b/apps/backend/src/http-client/http-client.service.spec.ts index b3856df5..511910ba 100644 --- a/apps/backend/src/http-client/http-client.service.spec.ts +++ b/apps/backend/src/http-client/http-client.service.spec.ts @@ -64,26 +64,47 @@ describe("HttpClientService", () => { expect(config.timeout).toBe(120000); }); - it("times out HTTP/2 requests using the connection timeout", async () => { + it("passes the configured headersTimeout to undici and translates its error", async () => { const service = await createService(); - if (typeof AbortSignal.timeout !== "function") { - (AbortSignal as any).timeout = () => new AbortController().signal; - } - - undiciRequestMock.mockImplementationOnce((_url: string, options: { signal?: AbortSignal }) => { - return new Promise((_resolve, reject) => { - options.signal?.addEventListener("abort", () => reject(new Error("aborted")), { once: true }); - }); + let receivedHeadersTimeout: number | undefined; + undiciRequestMock.mockImplementationOnce((_url: string, options: { headersTimeout?: number }) => { + receivedHeadersTimeout = options.headersTimeout; + const err = new Error("Headers Timeout Error") as Error & { code?: string }; + err.name = "HeadersTimeoutError"; + err.code = "UND_ERR_HEADERS_TIMEOUT"; + return Promise.reject(err); }); - vi.useFakeTimers(); + await expect(service.requestWithMetrics("http://example.com", { httpVersion: "2" })).rejects.toThrow( + "HTTP/2 connection/headers timed out after 25ms", + ); + + expect(receivedHeadersTimeout).toBe(25); + }); - const promise = service.requestWithMetrics("http://example.com", { httpVersion: "2" }); - const assertion = expect(promise).rejects.toThrow("HTTP/2 connection/headers timed out after 25ms"); - await vi.advanceTimersByTimeAsync(25); + it("keeps the request signal alive after the connect timeout window elapses", async () => { + const service = await createService(); - await assertion; + // Previously, connectTimeoutMs (25ms) was folded into the request signal, + // so any download lasting longer than 25ms was aborted mid-stream. The + // signal must now stay live until the transfer timeout or parent signal + // fires. + let sawAbortBeforeResolve = false; + undiciRequestMock.mockImplementationOnce(async (_url: string, options: { signal?: AbortSignal }) => { + await new Promise((r) => setTimeout(r, 75)); + sawAbortBeforeResolve = options.signal?.aborted === true; + async function* body() { + yield Buffer.from("ok"); + } + return { statusCode: 200, body: body() }; + }); + + const result = await service.requestWithMetrics("http://example.com", { httpVersion: "2" }); + + expect(sawAbortBeforeResolve).toBe(false); + expect(result.aborted).toBeUndefined(); + expect(result.metrics.statusCode).toBe(200); }); it("returns partial bytes and metrics when HTTP/2 download is aborted after headers", async () => { diff --git a/apps/backend/src/http-client/http-client.service.ts b/apps/backend/src/http-client/http-client.service.ts index cf845177..81140162 100644 --- a/apps/backend/src/http-client/http-client.service.ts +++ b/apps/backend/src/http-client/http-client.service.ts @@ -81,12 +81,11 @@ export class HttpClientService { let ttfbTime = 0; let statusCode = 0; - /** - * Dual-timeout strategy for HTTP/2 requests: - * 1. AbortSignal.timeout() - Undici's native timeout (10 min default) - * 2. AbortSignal.timeout() for connection/headers (10 sec default) - */ - const { signal, connectTimeoutSignal } = this.buildHttp2Signals(options.signal); + // Dual-timeout strategy for HTTP/2 requests: + // - `headersTimeout` (undici): scopes the connect + response-headers phase. + // - Combined AbortSignal: transfer-timeout ceiling + parent (job) signal. + const transferTimeoutSignal = AbortSignal.timeout(this.http2TimeoutMs); + const signal = options.signal ? anySignal([transferTimeoutSignal, options.signal]) : transferTimeoutSignal; const requestOptions: any = { method, headers: { @@ -94,6 +93,7 @@ export class HttpClientService { ...headers, }, signal, + headersTimeout: this.connectTimeoutMs, }; if (data) { @@ -105,7 +105,8 @@ export class HttpClientService { try { response = await undiciRequest(url, requestOptions); } catch (error) { - if (connectTimeoutSignal.aborted) { + // discern connection error from transfer error + if (isHeadersTimeoutError(error)) { throw new Error(`HTTP/2 connection/headers timed out after ${this.connectTimeoutMs}ms`); } throw error; @@ -285,26 +286,6 @@ export class HttpClientService { // Fallback for objects/arrays return Buffer.from(JSON.stringify(data)); } - - private buildHttp2Signals(parentSignal?: AbortSignal): { - signal: AbortSignal; - connectTimeoutSignal: AbortSignal; - } { - const transferTimeoutSignal = AbortSignal.timeout(this.http2TimeoutMs); - const connectTimeoutSignal = AbortSignal.timeout(this.connectTimeoutMs); - - if (parentSignal) { - return { - signal: anySignal([transferTimeoutSignal, connectTimeoutSignal, parentSignal]), - connectTimeoutSignal, - }; - } - - return { - signal: anySignal([transferTimeoutSignal, connectTimeoutSignal]), - connectTimeoutSignal, - }; - } } function isAbortLikeError(error: unknown): boolean { @@ -314,6 +295,15 @@ function isAbortLikeError(error: unknown): boolean { return false; } +/** + * Determines if a given error represents a "Headers Timeout" error. + */ +function isHeadersTimeoutError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + const code = (error as Error & { code?: string }).code; + return error.name === "HeadersTimeoutError" || code === "UND_ERR_HEADERS_TIMEOUT"; +} + function describeAbortReason(signal: AbortSignal | undefined, fallback: unknown): string { const reason = signal?.reason; if (reason instanceof Error && reason.message) return reason.message; From caab4e0b5f3d040855621b16a90939cc1a0d988b Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Thu, 23 Apr 2026 15:27:27 +0200 Subject: [PATCH 19/19] refactor(subgraph): collapse SAMPLE_ANON_PIECE_INDEXED/_ANY into builder The two anon-piece sampling queries differed only by the presence of withIPFSIndexing: true in the nested proofSet filter. Replace them with a single buildSampleAnonPieceQuery(pool) function that emits the shared shape and drops the filter when pool === "any". --- apps/backend/src/subgraph/queries.ts | 63 +++++-------------- apps/backend/src/subgraph/subgraph.service.ts | 4 +- 2 files changed, 17 insertions(+), 50 deletions(-) diff --git a/apps/backend/src/subgraph/queries.ts b/apps/backend/src/subgraph/queries.ts index fe42eef6..74802ddf 100644 --- a/apps/backend/src/subgraph/queries.ts +++ b/apps/backend/src/subgraph/queries.ts @@ -21,52 +21,18 @@ export const Queries = { } } `, - SAMPLE_ANON_PIECE_INDEXED: ` - query SampleAnonPieceIndexed( - $serviceProvider: Bytes! - $payer: Bytes! - $sampleKey: Bytes! - $minSize: BigInt! - $maxSize: BigInt! - ) { - _meta { - block { - number - } - } - roots( - first: 1 - orderBy: sampleKey - orderDirection: asc - where: { - sampleKey_gte: $sampleKey - removed: false - rawSize_gte: $minSize - rawSize_lte: $maxSize - proofSet_: { - fwssServiceProvider: $serviceProvider - fwssPayer_not: $payer - isActive: true - withIPFSIndexing: true - } - } - subgraphError: allow - ) { - rootId - cid - rawSize - ipfsRootCID - proofSet { - setId - withIPFSIndexing - fwssPayer - pdpPaymentEndEpoch - } - } - } - `, - SAMPLE_ANON_PIECE_ANY: ` - query SampleAnonPieceAny( +} as const; + +/** + * Build a sampleAnonPiece query scoped to the requested pool. The single + * piece of query shape that differs is whether the proofSet filter pins + * `withIPFSIndexing: true`; assembling the fragment here keeps the rest + * of the query and the returned selection set shared. + */ +export function buildSampleAnonPieceQuery(pool: "indexed" | "any"): string { + const indexingFilter = pool === "indexed" ? "withIPFSIndexing: true" : ""; + return ` + query SampleAnonPiece( $serviceProvider: Bytes! $payer: Bytes! $sampleKey: Bytes! @@ -91,6 +57,7 @@ export const Queries = { fwssServiceProvider: $serviceProvider fwssPayer_not: $payer isActive: true + ${indexingFilter} } } subgraphError: allow @@ -107,5 +74,5 @@ export const Queries = { } } } - `, -} as const; + `; +} diff --git a/apps/backend/src/subgraph/subgraph.service.ts b/apps/backend/src/subgraph/subgraph.service.ts index 233271f8..55359179 100644 --- a/apps/backend/src/subgraph/subgraph.service.ts +++ b/apps/backend/src/subgraph/subgraph.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { toStructuredError } from "../common/logging.js"; import type { IBlockchainConfig, IConfig } from "../config/app.config.js"; -import { Queries } from "./queries.js"; +import { buildSampleAnonPieceQuery, Queries } from "./queries.js"; import type { AnonCandidatePiece, GraphQLResponse, @@ -192,7 +192,7 @@ export class SubgraphService { return null; } - const query = params.pool === "indexed" ? Queries.SAMPLE_ANON_PIECE_INDEXED : Queries.SAMPLE_ANON_PIECE_ANY; + const query = buildSampleAnonPieceQuery(params.pool); const variables = { serviceProvider: params.serviceProvider.toLowerCase(), payer: params.payer.toLowerCase(),