|
1 | 1 | import winston from "winston"; |
| 2 | +import * as across from "@across-protocol/sdk"; |
2 | 3 | import { DataSource, entities, utils } from "@repo/indexer-database"; |
3 | 4 |
|
4 | 5 | export type BlockRangeInsertType = { |
@@ -32,6 +33,7 @@ export class BundleRepository extends utils.BaseRepository { |
32 | 33 | postgres: DataSource, |
33 | 34 | logger: winston.Logger, |
34 | 35 | throwError?: boolean, |
| 36 | + private chunkSize = 2000, |
35 | 37 | ) { |
36 | 38 | super(postgres, logger, throwError); |
37 | 39 | } |
@@ -371,4 +373,182 @@ export class BundleRepository extends utils.BaseRepository { |
371 | 373 |
|
372 | 374 | return (await executedUpdateQuery.execute())?.affected ?? 0; |
373 | 375 | } |
| 376 | + |
| 377 | + /** |
| 378 | + * Retrieves executed bundles that do not have events associated with them. |
| 379 | + * The query can be filtered by the block number and a limit on the number of results returned. |
| 380 | + * @param filters - Optional filters for the query. |
| 381 | + * @param filters.fromBlock - If provided, retrieves bundles where the proposal's block number is greater than this value. |
| 382 | + * @param limit - The maximum number of bundles to retrieve. |
| 383 | + * @returns An array of bundles that match the given criteria. |
| 384 | + */ |
| 385 | + public async getExecutedBundlesWithoutEventsAssociated( |
| 386 | + filters: { |
| 387 | + fromBlock?: number; |
| 388 | + }, |
| 389 | + limit = 5, |
| 390 | + ): Promise<entities.Bundle[]> { |
| 391 | + const bundleRepo = this.postgres.getRepository(entities.Bundle); |
| 392 | + const query = bundleRepo |
| 393 | + .createQueryBuilder("b") |
| 394 | + .select(["b", "proposal", "ranges"]) |
| 395 | + .leftJoinAndSelect("b.ranges", "ranges") |
| 396 | + .leftJoinAndSelect("b.proposal", "proposal") |
| 397 | + .where("b.status = :executed", { |
| 398 | + executed: entities.BundleStatus.Executed, |
| 399 | + }) |
| 400 | + .andWhere("b.eventsAssociated = false"); |
| 401 | + if (filters.fromBlock) { |
| 402 | + query.andWhere("proposal.blockNumber > :fromBlock", { |
| 403 | + fromBlock: filters.fromBlock, |
| 404 | + }); |
| 405 | + } |
| 406 | + return query.orderBy("proposal.blockNumber", "DESC").take(limit).getMany(); |
| 407 | + } |
| 408 | + |
| 409 | + /** |
| 410 | + * Updates the `eventsAssociated` flag to `true` for a specific bundle. |
| 411 | + * @param bundleId - The ID of the bundle to update. |
| 412 | + * @returns A promise that resolves when the update is complete. |
| 413 | + */ |
| 414 | + public async updateBundleEventsAssociatedFlag(bundleId: number) { |
| 415 | + const bundleRepo = this.postgres.getRepository(entities.Bundle); |
| 416 | + const updatedBundle = await bundleRepo |
| 417 | + .createQueryBuilder() |
| 418 | + .update() |
| 419 | + .set({ eventsAssociated: true }) |
| 420 | + .where("id = :id", { id: bundleId }) |
| 421 | + .execute(); |
| 422 | + return updatedBundle.affected; |
| 423 | + } |
| 424 | + |
| 425 | + /** |
| 426 | + * Stores bundle events relating them to a given bundle. |
| 427 | + * @param bundleData The reconstructed bundle data. |
| 428 | + * @param bundleId ID of the bundle to associate these events with. |
| 429 | + * @returns A promise that resolves when all the events have been inserted into the database. |
| 430 | + */ |
| 431 | + public async storeBundleEvents( |
| 432 | + bundleData: across.interfaces.LoadDataReturnValue, |
| 433 | + bundleId: number, |
| 434 | + ) { |
| 435 | + const eventsRepo = this.postgres.getRepository(entities.BundleEvent); |
| 436 | + |
| 437 | + // Store bundle deposits |
| 438 | + const deposits = this.formatBundleEvents( |
| 439 | + entities.BundleEventType.Deposit, |
| 440 | + bundleData.bundleDepositsV3, |
| 441 | + bundleId, |
| 442 | + ); |
| 443 | + const chunkedDeposits = across.utils.chunk(deposits, this.chunkSize); |
| 444 | + await Promise.all( |
| 445 | + chunkedDeposits.map((eventsChunk) => eventsRepo.insert(eventsChunk)), |
| 446 | + ); |
| 447 | + |
| 448 | + // Store bundle refunded deposits |
| 449 | + const expiredDeposits = this.formatBundleEvents( |
| 450 | + entities.BundleEventType.ExpiredDeposit, |
| 451 | + bundleData.expiredDepositsToRefundV3, |
| 452 | + bundleId, |
| 453 | + ); |
| 454 | + const chunkedRefunds = across.utils.chunk(expiredDeposits, this.chunkSize); |
| 455 | + await Promise.all( |
| 456 | + chunkedRefunds.map((eventsChunk) => eventsRepo.insert(eventsChunk)), |
| 457 | + ); |
| 458 | + |
| 459 | + // Store bundle slow fills |
| 460 | + const slowFills = this.formatBundleEvents( |
| 461 | + entities.BundleEventType.SlowFill, |
| 462 | + bundleData.bundleSlowFillsV3, |
| 463 | + bundleId, |
| 464 | + ); |
| 465 | + const chunkedSlowFills = across.utils.chunk(slowFills, this.chunkSize); |
| 466 | + await Promise.all( |
| 467 | + chunkedSlowFills.map((eventsChunk) => eventsRepo.insert(eventsChunk)), |
| 468 | + ); |
| 469 | + |
| 470 | + // Store bundle unexecutable slow fills |
| 471 | + const unexecutableSlowFills = this.formatBundleEvents( |
| 472 | + entities.BundleEventType.UnexecutableSlowFill, |
| 473 | + bundleData.unexecutableSlowFills, |
| 474 | + bundleId, |
| 475 | + ); |
| 476 | + const chunkedUnexecutableSlowFills = across.utils.chunk( |
| 477 | + unexecutableSlowFills, |
| 478 | + this.chunkSize, |
| 479 | + ); |
| 480 | + await Promise.all( |
| 481 | + chunkedUnexecutableSlowFills.map((eventsChunk) => |
| 482 | + eventsRepo.insert(eventsChunk), |
| 483 | + ), |
| 484 | + ); |
| 485 | + |
| 486 | + // Store bundle fills |
| 487 | + const fills = this.formatBundleFillEvents( |
| 488 | + entities.BundleEventType.Fill, |
| 489 | + bundleData.bundleFillsV3, |
| 490 | + bundleId, |
| 491 | + ); |
| 492 | + const chunkedFills = across.utils.chunk(fills, this.chunkSize); |
| 493 | + await Promise.all( |
| 494 | + chunkedFills.map((eventsChunk) => eventsRepo.insert(eventsChunk)), |
| 495 | + ); |
| 496 | + |
| 497 | + return { |
| 498 | + deposits: deposits.length, |
| 499 | + expiredDeposits: expiredDeposits.length, |
| 500 | + slowFills: slowFills.length, |
| 501 | + unexecutableSlowFills: unexecutableSlowFills.length, |
| 502 | + fills: fills.length, |
| 503 | + }; |
| 504 | + } |
| 505 | + |
| 506 | + private formatBundleEvents( |
| 507 | + eventsType: entities.BundleEventType, |
| 508 | + bundleEvents: |
| 509 | + | across.interfaces.BundleDepositsV3 |
| 510 | + | across.interfaces.BundleSlowFills |
| 511 | + | across.interfaces.BundleExcessSlowFills |
| 512 | + | across.interfaces.ExpiredDepositsToRefundV3, |
| 513 | + bundleId: number, |
| 514 | + ): { |
| 515 | + bundleId: number; |
| 516 | + relayHash: string; |
| 517 | + type: entities.BundleEventType; |
| 518 | + }[] { |
| 519 | + return Object.values(bundleEvents).flatMap((tokenEvents) => |
| 520 | + Object.values(tokenEvents).flatMap((events) => |
| 521 | + events.map((event) => { |
| 522 | + return { |
| 523 | + bundleId, |
| 524 | + relayHash: across.utils.getRelayHashFromEvent(event), |
| 525 | + type: eventsType, |
| 526 | + }; |
| 527 | + }), |
| 528 | + ), |
| 529 | + ); |
| 530 | + } |
| 531 | + |
| 532 | + private formatBundleFillEvents( |
| 533 | + eventsType: entities.BundleEventType.Fill, |
| 534 | + bundleEvents: across.interfaces.BundleFillsV3, |
| 535 | + bundleId: number, |
| 536 | + ): { |
| 537 | + bundleId: number; |
| 538 | + relayHash: string; |
| 539 | + type: entities.BundleEventType.Fill; |
| 540 | + }[] { |
| 541 | + return Object.entries(bundleEvents).flatMap(([chainId, tokenEvents]) => |
| 542 | + Object.values(tokenEvents).flatMap((fillsData) => |
| 543 | + fillsData.fills.map((event) => { |
| 544 | + return { |
| 545 | + bundleId, |
| 546 | + relayHash: across.utils.getRelayHashFromEvent(event), |
| 547 | + type: eventsType, |
| 548 | + repaymentChainId: Number(chainId), |
| 549 | + }; |
| 550 | + }), |
| 551 | + ), |
| 552 | + ); |
| 553 | + } |
374 | 554 | } |
0 commit comments