Skip to content

Conversation

@NikolasHaimerl
Copy link
Contributor

@NikolasHaimerl NikolasHaimerl commented Dec 8, 2025

This PR introduces the Deposit entity and the updateDeposits functionality.
This new table serves as an aggregation layer for all cross-chain activities (Across V3, CCTP, and LayerZero OFT), so that we can serve the deposits without a hardcoded limit and with a small response time.
Instead of querying three distinct protocol tables and merging them in-memory (which scales poorly), we now maintain a single table for deposits.

Database Design: The Deposit Entity

The Deposit table is designed to store the relevant information for fast indexing and querying without containing non-relevant information that can be sourced from other tables. .
It duplicates only the fields necessary for sorting, filtering, and pagination, while delegating protocol-specific data to linked foreign tables.The fields that exist in the table are based on the current requirements of the deposits endpoint. Ideally, the fields that currently exist can be used to index the relevant rows fast (as it is currently done in memory for the endpoint) and then additional information can beadded by merging the relevant columns through the stored foreign keys.

Table Structure

@Entity()
export class Deposit {
  @PrimaryGeneratedColumn() id: number;

  // Polymorphic ID used for upsert deduplication
  @Column({ unique: true }) uniqueId: string;

  // The discriminator
  @Column({ type: "enum", enum: DepositType }) type: DepositType; // ACROSS | CCTP | OFT

  // Global sorting field
  @Index() @Column() blockTimestamp: Date;

  // Global filtering fields
  @Column() originChainId: string;
  @Column() destinationChainId: string;
  @Index() @Column({ nullable: true }) depositor: string;
  @Index() @Column({ nullable: true }) recipient: string;

  // Status for fast "Unfilled" lookups
  @Index() @Column({ default: DepositStatus.PENDING }) status: DepositStatus;

  // Exclusive One-to-One Foreign Keys
  @OneToOne(() => V3FundsDeposited, { nullable: true }) v3FundsDeposited: V3FundsDeposited;
  @OneToOne(() => FilledV3Relay, { nullable: true }) filledV3Relay: FilledV3Relay;
  @OneToOne(() => DepositForBurn, { nullable: true }) depositForBurn: DepositForBurn;
  @OneToOne(() => MintAndWithdraw, { nullable: true }) mintAndWithdraw: MintAndWithdraw;
  // ... OFT keys
}
Field Purpose Why is it here?
uniqueId Idempotency Allows stitching source/destination events regardless of arrival order.
• Across: internalHash
• CCTP: ${nonce}-${destinationChainId}
• OFT: guid
blockTimestamp Sorting Required for UI ORDER BY timestamp DESC. Enables O(1) pagination instead of merging 3 tables in-memory.
status Filtering Users frequently filter by “Pending”. Indexed for instant lookup.
depositor History Needed for “My Transactions”. Nullable because a fill may arrive before a deposit (“Orphan Fill”).
Foreign Keys Data Integrity Enforces referential integrity and allows efficient eager-loading instead of JSON blobs.

Ingestion Logic: updateDeposits

Every event is processed using: INSERT ... ON CONFLICT (uniqueId) DO UPDATE

We cannot guarantee that source events arrive before destination events.

Behavior

Scenario Result
Source event arrives first Create record → status = PENDING
Destination event arrives first Create record → status = FILLED (Orphan Fill)
Second event arrives Update existing row → merge fields → status = FILLED

- Replace separate protocol queries with single query against new Deposit table
- Add Deposit entity with foreign keys to V3FundsDeposited, DepositForBurn, OFTSent events
- Implement updateDeposits helper to maintain deposit index across all protocols
- Simplify getDeposits method from 440+ lines to 200+ lines with improved performance
- Add proper TypeORM joins and filtering logic for cross-protocol deposit queries
- Create migration for new deposit table with optimized indices for common query patterns
OFT = "oft",
}

export enum DepositStatus {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deposits endpoint only cares about whether a deposit is finished or not. All other statuses are irrelevant to the deposit endpoint.

@Index("IX_deposits_depositor_timestamp", ["depositor", "blockTimestamp"])
@Index("IX_deposits_recipient_timestamp", ["recipient", "blockTimestamp"])
// 3. Status Index: Fast "Unfilled" lookups
@Index("IX_deposits_status_timestamp", ["status", "blockTimestamp"])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indices are chosen based on the current behaviour of the API to make these lookups as fast as possible.

});

const chunkedEvents = across.utils.chunk(formattedEvents, chunkSize);
const chunkedEvents: Partial<TEntity>[][] = across.utils.chunk(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need explicit typing for updating the saved events in storeEvents functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants