Skip to content

Commit 64d0329

Browse files
authored
feat: production release πŸš€ (#314)
1 parent 30a0a57 commit 64d0329

32 files changed

+1013
-204
lines changed

β€Žpackages/indexer-api/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"author": "",
2323
"license": "ISC",
2424
"dependencies": {
25-
"@across-protocol/sdk": "~4.2.1",
25+
"@across-protocol/sdk": "^4.2.8",
2626
"@repo/error-handling": "workspace:*",
2727
"@repo/indexer": "workspace:*",
2828
"@repo/indexer-database": "workspace:*",

β€Žpackages/indexer-api/src/controllers/deposits.tsβ€Ž

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ export class DepositsController {
3838
}
3939
};
4040

41+
public getDeposit = async (
42+
req: Request,
43+
res: Response,
44+
next: NextFunction,
45+
) => {
46+
try {
47+
const params = s.create(req.query, DepositParams);
48+
const result = await this.service.getDeposit(params);
49+
return res.json(result);
50+
} catch (err) {
51+
next(err);
52+
}
53+
};
54+
4155
public getUnfilledDeposits = async (
4256
req: Request,
4357
res: Response,

β€Žpackages/indexer-api/src/dtos/deposits.dto.tsβ€Ž

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as s from "superstruct";
2+
import { utils } from "@across-protocol/sdk";
23
import { entities } from "@repo/indexer-database";
34

45
const stringToInt = s.coerce(s.number(), s.string(), (value) => {
@@ -9,13 +10,28 @@ const stringToInt = s.coerce(s.number(), s.string(), (value) => {
910
return parseInt(value, 10);
1011
});
1112

13+
const parseAddressField = s.coerce(s.string(), s.string(), (value) => {
14+
// Try to parse as evm address
15+
if (utils.isValidEvmAddress(value.toLowerCase())) {
16+
return utils.toAddress(value.toLowerCase());
17+
} else {
18+
// Try to parse as svm address
19+
try {
20+
return utils.SvmAddress.from(value).toBase58();
21+
} catch (error) {
22+
// Otherwise use original value
23+
return value;
24+
}
25+
}
26+
});
27+
1228
export const DepositsParams = s.object({
13-
depositor: s.optional(s.string()),
14-
recipient: s.optional(s.string()),
29+
depositor: s.optional(parseAddressField),
30+
recipient: s.optional(parseAddressField),
1531
originChainId: s.optional(stringToInt),
1632
destinationChainId: s.optional(stringToInt),
17-
inputToken: s.optional(s.string()),
18-
outputToken: s.optional(s.string()),
33+
inputToken: s.optional(parseAddressField),
34+
outputToken: s.optional(parseAddressField),
1935
integratorId: s.optional(s.string()),
2036
status: s.optional(s.enums(Object.values(entities.RelayStatus))),
2137
// some kind of pagination options, skip could be the start point

β€Žpackages/indexer-api/src/routers/deposits.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function getRouter(db: DataSource, redis: Redis): Router {
99
const service = new DepositsService(db, redis);
1010
const controller = new DepositsController(service);
1111
router.get("/deposits", controller.getDeposits);
12+
router.get("/deposit", controller.getDeposit);
1213
router.get("/deposit/status", controller.getDepositStatus);
1314
router.get("/deposits/unfilled", controller.getUnfilledDeposits);
1415
router.get("/deposits/filled", controller.getFilledDeposits);

β€Žpackages/indexer-api/src/services/deposits.tsβ€Ž

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,108 @@ export class DepositsService {
285285
return result;
286286
}
287287

288+
public async getDeposit(params: DepositParams) {
289+
// in the validation rules each of these params are marked as optional
290+
// but we need to check that at least one of them is present
291+
if (
292+
!(
293+
(params.depositId && params.originChainId) ||
294+
params.depositTxHash ||
295+
params.relayDataHash
296+
)
297+
) {
298+
throw new IncorrectQueryParamsException();
299+
}
300+
301+
// construct cache key
302+
const cacheKey = this.getDepositCacheKey(params);
303+
const cachedData = await this.redis.get(cacheKey);
304+
305+
if (cachedData) {
306+
return JSON.parse(cachedData);
307+
}
308+
309+
// no cached data, so we need to query the database
310+
const repo = this.db.getRepository(entities.RelayHashInfo);
311+
const queryBuilder = repo.createQueryBuilder("rhi");
312+
313+
queryBuilder.leftJoinAndSelect(
314+
entities.V3FundsDeposited,
315+
"deposit",
316+
"rhi.depositEventId = deposit.id",
317+
);
318+
queryBuilder.leftJoinAndSelect(
319+
entities.SwapBeforeBridge,
320+
"swap",
321+
"swap.id = rhi.swapBeforeBridgeEventId",
322+
);
323+
queryBuilder.leftJoinAndSelect(
324+
entities.FilledV3Relay,
325+
"fill",
326+
"fill.id = rhi.fillEventId",
327+
);
328+
329+
if (params.depositId && params.originChainId) {
330+
queryBuilder.andWhere(
331+
"rhi.depositId = :depositId AND rhi.originChainId = :originChainId",
332+
{
333+
depositId: params.depositId,
334+
originChainId: params.originChainId,
335+
},
336+
);
337+
}
338+
339+
if (params.depositTxHash) {
340+
queryBuilder.andWhere("rhi.depositTxHash = :depositTxHash", {
341+
depositTxHash: params.depositTxHash,
342+
});
343+
}
344+
345+
if (params.relayDataHash) {
346+
queryBuilder.andWhere("rhi.relayHash = :relayDataHash", {
347+
relayDataHash: params.relayDataHash,
348+
});
349+
}
350+
351+
const matchingRelays = await queryBuilder
352+
.orderBy("rhi.depositEventId", "ASC")
353+
.select([
354+
...DepositFields,
355+
...RelayHashInfoFields,
356+
...SwapBeforeBridgeFields,
357+
...FilledRelayFields,
358+
])
359+
.execute();
360+
const numberMatchingRelays = matchingRelays.length;
361+
if (numberMatchingRelays === 0) throw new DepositNotFoundException();
362+
const relay = matchingRelays[params.index];
363+
if (!relay) {
364+
throw new IndexParamOutOfRangeException(
365+
`Index ${params.index} out of range. Index must be between 0 and ${numberMatchingRelays - 1}`,
366+
);
367+
}
368+
369+
const result = {
370+
deposit: {
371+
...relay,
372+
},
373+
pagination: {
374+
currentIndex: params.index,
375+
maxIndex: numberMatchingRelays - 1,
376+
},
377+
};
378+
379+
if (this.shouldCacheDepositResponse(relay)) {
380+
await this.redis.set(
381+
cacheKey,
382+
JSON.stringify(result),
383+
"EX",
384+
this.getDepositCacheTTLSeconds(relay),
385+
);
386+
}
387+
return result;
388+
}
389+
288390
public async getUnfilledDeposits(
289391
params: FilterDepositsParams,
290392
): Promise<ParsedDepositReturnType[]> {
@@ -493,6 +595,27 @@ export class DepositsService {
493595
}
494596
}
495597

598+
private getDepositCacheTTLSeconds(deposit: DepositReturnType) {
599+
const minute = 60;
600+
const hour = 60 * minute;
601+
const day = 24 * hour;
602+
603+
if (
604+
deposit.status === entities.RelayStatus.Filled &&
605+
deposit.depositBlockTimestamp &&
606+
deposit.fillBlockTimestamp &&
607+
deposit.bridgeFeeUsd
608+
) {
609+
return hour;
610+
}
611+
612+
if (deposit.status === entities.RelayStatus.Refunded) {
613+
return hour;
614+
}
615+
616+
return 0;
617+
}
618+
496619
private shouldCacheDepositStatusResponse(status: entities.RelayStatus) {
497620
return [
498621
entities.RelayStatus.Expired,
@@ -502,6 +625,23 @@ export class DepositsService {
502625
].includes(status);
503626
}
504627

628+
private shouldCacheDepositResponse(deposit: DepositReturnType) {
629+
if (
630+
deposit.status === entities.RelayStatus.Filled &&
631+
deposit.depositBlockTimestamp &&
632+
deposit.fillBlockTimestamp &&
633+
deposit.bridgeFeeUsd
634+
) {
635+
return true;
636+
}
637+
638+
if (deposit.status === entities.RelayStatus.Refunded) {
639+
return true;
640+
}
641+
642+
return false;
643+
}
644+
505645
private getDepositStatusCacheKey(params: DepositParams) {
506646
if (params.depositId && params.originChainId) {
507647
return `depositStatus-${params.depositId}-${params.originChainId}-${params.index}`;
@@ -519,4 +659,20 @@ export class DepositsService {
519659
"Could not get deposit status: could not locate cache data",
520660
);
521661
}
662+
663+
private getDepositCacheKey(params: DepositParams) {
664+
if (params.depositId && params.originChainId) {
665+
return `deposit-${params.depositId}-${params.originChainId}-${params.index}`;
666+
}
667+
if (params.depositTxHash) {
668+
return `deposit-${params.depositTxHash}-${params.index}`;
669+
}
670+
if (params.relayDataHash) {
671+
return `deposit-${params.relayDataHash}-${params.index}`;
672+
}
673+
674+
// in theory this should never happen because we have already checked
675+
// that at least one of the params is present
676+
throw new Error("Could not get deposit: could not locate cache data");
677+
}
522678
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { expect } from "chai";
2+
import request from "supertest";
3+
import { ExpressApp } from "../express-app";
4+
import express from "express";
5+
import {
6+
createDataSource,
7+
DataSource,
8+
entities,
9+
fixtures,
10+
} from "@repo/indexer-database";
11+
import Redis from "ioredis";
12+
import * as Indexer from "@repo/indexer";
13+
import * as utils from "../utils";
14+
import * as routers from "../routers";
15+
16+
describe("/deposit", () => {
17+
let app: express.Express;
18+
let dataSource: DataSource;
19+
let redis: Redis;
20+
let depositsFixture: fixtures.FundsDepositedFixture;
21+
let relayHashInfoFixture: fixtures.RelayHashInfoFixture;
22+
23+
before(async () => {
24+
// Set up database and Redis
25+
const databaseConfig = utils.getPostgresConfig(process.env);
26+
dataSource = await createDataSource(databaseConfig).initialize();
27+
28+
const redisConfig = Indexer.parseRedisConfig(process.env);
29+
redis = new Redis(redisConfig);
30+
31+
// Initialize fixtures
32+
depositsFixture = new fixtures.FundsDepositedFixture(dataSource);
33+
relayHashInfoFixture = new fixtures.RelayHashInfoFixture(dataSource);
34+
await depositsFixture.deleteAllDeposits();
35+
await relayHashInfoFixture.deleteAllRelayHashInfoRows();
36+
37+
// Initialize the Express app with the deposits router
38+
const depositsRouter = routers.deposits.getRouter(dataSource, redis);
39+
app = ExpressApp({ deposits: depositsRouter });
40+
});
41+
42+
after(async () => {
43+
// Clean up resources
44+
await depositsFixture.deleteAllDeposits();
45+
await dataSource.destroy();
46+
await redis.quit();
47+
});
48+
49+
it("should return 200 and one deposit by depositId and originChainId", async () => {
50+
// Insert a test deposit
51+
const [deposit] = await depositsFixture.insertDeposits([]);
52+
const relayHashInfoData = {
53+
id: 1,
54+
depositId: deposit.depositId,
55+
originChainId: deposit.originChainId,
56+
depositEventId: deposit.id,
57+
status: entities.RelayStatus.Unfilled,
58+
};
59+
await relayHashInfoFixture.insertRelayHashInfos([relayHashInfoData]);
60+
const response = await request(app).get("/deposit").query({
61+
depositId: deposit.depositId,
62+
originChainId: deposit.originChainId,
63+
});
64+
console.log(response.body);
65+
expect(response.status).to.equal(200);
66+
expect(response.body.deposit.id).to.equal(deposit.id);
67+
});
68+
});

β€Žpackages/indexer-database/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"author": "",
2626
"license": "ISC",
2727
"dependencies": {
28-
"@across-protocol/sdk": "~4.2.1",
28+
"@across-protocol/sdk": "^4.2.8",
2929
"pg": "^8.4.0",
3030
"reflect-metadata": "^0.1.13",
3131
"superstruct": "2.0.3-1",

β€Žpackages/indexer-database/src/entities/evm/ExecutedRelayerRefundRoot.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ExecutedRelayerRefundRoot {
4040
@Column({ nullable: true })
4141
deferredRefunds: boolean;
4242

43-
@Column()
43+
@Column({ nullable: true })
4444
caller: string;
4545

4646
@Column()

β€Žpackages/indexer-database/src/entities/evm/FilledV3Relay.tsβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import { RelayHashInfo } from "../RelayHashInfo";
1313
@Entity({ schema: "evm" })
1414
@Unique("UK_filledV3Relay_internalHash", ["internalHash"])
1515
@Index("IX_filledV3Relay_blockTimestamp", ["blockTimestamp"])
16+
@Index("IX_filledV3Relay_relayer", ["relayer"])
17+
@Index("IX_filledV3Relay_destinationChainId", ["destinationChainId"])
18+
@Index("IX_filledV3Relay_depositId_originChainId", [
19+
"depositId",
20+
"originChainId",
21+
])
1622
export class FilledV3Relay {
1723
@PrimaryGeneratedColumn()
1824
id: number;

β€Žpackages/indexer-database/src/entities/evm/RequestedSpeedUpV3Deposit.tsβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export class RequestedSpeedUpV3Deposit {
1717
@PrimaryGeneratedColumn()
1818
id: number;
1919

20-
@Column()
21-
originChainId: number;
20+
@Column({ type: "bigint" })
21+
originChainId: string;
2222

2323
@Column({ type: "decimal" })
2424
depositId: string;

0 commit comments

Comments
Β (0)