Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions packages/indexer-database/src/entities/Deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
Unique,
} from "typeorm";

// Import your existing entities
import { V3FundsDeposited } from "./evm/V3FundsDeposited";
import { FilledV3Relay } from "./evm/FilledV3Relay";
import { DepositForBurn } from "./evm/DepositForBurn";
import { MintAndWithdraw } from "./evm/MintAndWithdraw";
import { OFTSent } from "./evm/OftSent";
import { OFTReceived } from "./evm/OftReceived";

export enum DepositType {
ACROSS = "across",
CCTP = "cctp",
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.

PENDING = "pending",
FILLED = "filled",
}

@Entity({ schema: "public" })
@Unique("UK_deposits_uniqueId", ["uniqueId"])
// 1. Global Feed Index: Instant sorting by time
@Index("IX_deposits_blockTimestamp", ["blockTimestamp"])
// 2. User History Indices: Instant filtering by user + sorting
@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.

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

/**
* The ID which stitches together all the relevant events for a given transfer type.
* OFT: guid
* CCTP: nonce-destinationChainId
* Across: relayHash / internalHash
*/
@Column()
uniqueId: string;

@Column({ type: "enum", enum: DepositType })
type: DepositType;

@Column({ type: "enum", enum: DepositStatus, default: DepositStatus.PENDING })
status: DepositStatus;

// --- Denormalized Search Fields ---

/**
* The timestamp of the first event seen for a given uniqueId.
*/
@Column()
blockTimestamp: Date;

@Column({ type: "bigint" })
originChainId: string;

@Column({ type: "bigint" })
destinationChainId: string;

/**
* Nullable because an Orphan Fill (e.g. OFTReceived) does not know the depositor.
* We update this when the source event arrives.
*/
@Column({ nullable: true })
depositor: string;

@Column({ nullable: true })
recipient: string;

// --- Foreign Keys (Nullable for Orphan Support) ---

// Across V3
@Column({ nullable: true })
v3FundsDepositedId: number | null;

@OneToOne(() => V3FundsDeposited, { nullable: true })
@JoinColumn({ name: "v3FundsDepositedId" })
v3FundsDeposited: V3FundsDeposited;

@Column({ nullable: true })
filledV3RelayId: number | null;

@OneToOne(() => FilledV3Relay, { nullable: true })
@JoinColumn({ name: "filledV3RelayId" })
filledV3Relay: FilledV3Relay;

// CCTP
@Column({ nullable: true })
depositForBurnId: number | null;

@OneToOne(() => DepositForBurn, { nullable: true })
@JoinColumn({ name: "depositForBurnId" })
depositForBurn: DepositForBurn;

@Column({ nullable: true })
mintAndWithdrawId: number | null;

@OneToOne(() => MintAndWithdraw, { nullable: true })
@JoinColumn({ name: "mintAndWithdrawId" })
mintAndWithdraw: MintAndWithdraw;

// OFT
@Column({ nullable: true })
oftSentId: number | null;

@OneToOne(() => OFTSent, { nullable: true })
@JoinColumn({ name: "oftSentId" })
oftSent: OFTSent;

@Column({ nullable: true })
oftReceivedId: number | null;

@OneToOne(() => OFTReceived, { nullable: true })
@JoinColumn({ name: "oftReceivedId" })
oftReceived: OFTReceived;

// --- Metadata ---

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
3 changes: 3 additions & 0 deletions packages/indexer-database/src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ export * from "./evm/FallbackHyperEVMFlowCompleted";
export * from "./evm/SponsoredAccountActivation";
export * from "./evm/SwapFlowInitialized";
export * from "./evm/SwapFlowFinalized";

// Deposits
export * from "./Deposit";
2 changes: 2 additions & 0 deletions packages/indexer-database/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export const createDataSource = (config: DatabaseConfig): DataSource => {
entities.SponsoredAccountActivation,
entities.SwapFlowInitialized,
entities.SwapFlowFinalized,
// Deposits
entities.Deposit,
],
migrationsTableName: "_migrations",
migrations: ["migrations/*.ts"],
Expand Down
123 changes: 123 additions & 0 deletions packages/indexer-database/src/migrations/1764868811392-Deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateDepositTable1764868811392 implements MigrationInterface {
name = "CreateDepositTable1764868811392";

public async up(queryRunner: QueryRunner): Promise<void> {
// Create Enums
await queryRunner.query(
`CREATE TYPE "public"."deposit_type_enum" AS ENUM('across', 'cctp', 'oft')`,
);
await queryRunner.query(
`CREATE TYPE "public"."deposit_status_enum" AS ENUM('pending', 'filled')`,
);

// Create Table
await queryRunner.query(
`CREATE TABLE "public"."deposit" (
"id" SERIAL NOT NULL,
"uniqueId" character varying NOT NULL,
"type" "public"."deposit_type_enum" NOT NULL,
"status" "public"."deposit_status_enum" NOT NULL DEFAULT 'pending',
"blockTimestamp" TIMESTAMP NOT NULL,
"originChainId" bigint NOT NULL,
"destinationChainId" bigint NOT NULL,
"depositor" character varying,
"recipient" character varying,
"v3FundsDepositedId" integer,
"filledV3RelayId" integer,
"depositForBurnId" integer,
"mintAndWithdrawId" integer,
"oftSentId" integer,
"oftReceivedId" integer,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "UK_deposits_uniqueId" UNIQUE ("uniqueId"),
CONSTRAINT "PK_deposit" PRIMARY KEY ("id")
)`,
);

// Create Indices
await queryRunner.query(
`CREATE INDEX "IX_deposits_blockTimestamp" ON "public"."deposit" ("blockTimestamp")`,
);
// User history lookups
await queryRunner.query(
`CREATE INDEX "IX_deposits_depositor_timestamp" ON "public"."deposit" ("depositor", "blockTimestamp")`,
);
await queryRunner.query(
`CREATE INDEX "IX_deposits_recipient_timestamp" ON "public"."deposit" ("recipient", "blockTimestamp")`,
);
// Status lookups (for finding unfilled deposits)
await queryRunner.query(
`CREATE INDEX "IX_deposits_status_timestamp" ON "public"."deposit" ("status", "blockTimestamp")`,
);

// Add Foreign Keys

// Across
await queryRunner.query(
`ALTER TABLE "public"."deposit" ADD CONSTRAINT "FK_deposit_v3FundsDeposited" FOREIGN KEY ("v3FundsDepositedId") REFERENCES "evm"."v3_funds_deposited"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" ADD CONSTRAINT "FK_deposit_filledV3Relay" FOREIGN KEY ("filledV3RelayId") REFERENCES "evm"."filled_v3_relay"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);

// CCTP
await queryRunner.query(
`ALTER TABLE "public"."deposit" ADD CONSTRAINT "FK_deposit_depositForBurn" FOREIGN KEY ("depositForBurnId") REFERENCES "evm"."deposit_for_burn"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" ADD CONSTRAINT "FK_deposit_mintAndWithdraw" FOREIGN KEY ("mintAndWithdrawId") REFERENCES "evm"."mint_and_withdraw"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);

// OFT
await queryRunner.query(
`ALTER TABLE "public"."deposit" ADD CONSTRAINT "FK_deposit_oftSent" FOREIGN KEY ("oftSentId") REFERENCES "evm"."oft_sent"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" ADD CONSTRAINT "FK_deposit_oftReceived" FOREIGN KEY ("oftReceivedId") REFERENCES "evm"."oft_received"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Drop Foreign Keys
await queryRunner.query(
`ALTER TABLE "public"."deposit" DROP CONSTRAINT "FK_deposit_oftReceived"`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" DROP CONSTRAINT "FK_deposit_oftSent"`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" DROP CONSTRAINT "FK_deposit_mintAndWithdraw"`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" DROP CONSTRAINT "FK_deposit_depositForBurn"`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" DROP CONSTRAINT "FK_deposit_filledV3Relay"`,
);
await queryRunner.query(
`ALTER TABLE "public"."deposit" DROP CONSTRAINT "FK_deposit_v3FundsDeposited"`,
);

// Drop Indices
await queryRunner.query(
`DROP INDEX "public"."IX_deposits_status_timestamp"`,
);
await queryRunner.query(
`DROP INDEX "public"."IX_deposits_recipient_timestamp"`,
);
await queryRunner.query(
`DROP INDEX "public"."IX_deposits_depositor_timestamp"`,
);
await queryRunner.query(`DROP INDEX "public"."IX_deposits_blockTimestamp"`);

// Drop Table
await queryRunner.query(`DROP TABLE "public"."deposit"`);

// Drop Enums
await queryRunner.query(`DROP TYPE "public"."deposit_status_enum"`);
await queryRunner.query(`DROP TYPE "public"."deposit_type_enum"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ethers, providers, Transaction } from "ethers";
import * as across from "@across-protocol/sdk";
import { CHAIN_IDs, TEST_NETWORKS } from "@across-protocol/constants";
import { formatFromAddressToChainFormat } from "../../utils";
import { updateDeposits } from "../../database/Deposits";
import {
BlockRange,
SimpleTransferFlowCompletedLog,
Expand Down Expand Up @@ -700,6 +701,36 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler {
),
]);

// We update the deposits table if we see new burn or mint events
await Promise.all([
...savedBurnEvents.map(({ depositForBurnEvent, messageSentEvent }) =>
updateDeposits({
dataSource: (this.cctpRepository as any).postgres,
depositUpdate: {
cctp: {
burn: {
depositForBurn: depositForBurnEvent.data,
messageSent: messageSentEvent.data,
},
},
},
}),
),
...savedMintEvents.map(({ mintAndWithdrawEvent, messageReceivedEvent }) =>
updateDeposits({
dataSource: (this.cctpRepository as any).postgres,
depositUpdate: {
cctp: {
mint: {
mintAndWithdraw: mintAndWithdrawEvent.data,
messageReceived: messageReceivedEvent.data,
},
},
},
}),
),
]);

return {
savedBurnEvents,
savedMintEvents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ethers, providers, Transaction } from "ethers";
import * as across from "@across-protocol/sdk";

import { entities, SaveQueryResult } from "@repo/indexer-database";

import { updateDeposits } from "../../database/Deposits";
import {
ArbitraryActionsExecutedLog,
BlockRange,
Expand Down Expand Up @@ -417,6 +417,30 @@ export class OFTIndexerDataHandler implements IndexerDataHandler {
),
]);

// We update the deposits table if we see new sent or received events
await Promise.all([
...savedOftSentEvents.map((oftSent) =>
updateDeposits({
dataSource: (this.oftRepository as any).postgres,
depositUpdate: {
oft: {
sent: oftSent.data,
},
},
}),
),
...savedOftReceivedEvents.map((oftReceived) =>
updateDeposits({
dataSource: (this.oftRepository as any).postgres,
depositUpdate: {
oft: {
received: oftReceived.data,
},
},
}),
),
]);

return {
oftSentEvents: savedOftSentEvents,
oftReceivedEvents: savedOftReceivedEvents,
Expand Down
Loading