@pafi-dev/issuer-postgres 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -79,7 +79,22 @@ __decorateClass([
79
79
  ], LockedMintEntity.prototype, "userOpHash", 2);
80
80
  LockedMintEntity = __decorateClass([
81
81
  Entity({ name: "locked_mint_requests" }),
82
- Index(["userAddress", "status"])
82
+ Index("IDX_locked_mint_user_token_status_expires", [
83
+ "userAddress",
84
+ "tokenAddress",
85
+ "status",
86
+ "expiresAt"
87
+ ]),
88
+ Index("IDX_locked_mint_user_token_amount_status", [
89
+ "userAddress",
90
+ "tokenAddress",
91
+ "amount",
92
+ "status"
93
+ ]),
94
+ Index("IDX_locked_mint_pending_expires", ["expiresAt"], {
95
+ where: `"status" = 'PENDING'`
96
+ }),
97
+ Index("IDX_locked_mint_user_op_hash", ["userOpHash"])
83
98
  ], LockedMintEntity);
84
99
 
85
100
  // src/entities/pending-credit.entity.ts
@@ -244,10 +259,61 @@ __decorateClass([
244
259
  ], LedgerJournalEntity.prototype, "createdAt", 2);
245
260
  LedgerJournalEntity = __decorateClass([
246
261
  Entity4({ name: "ledger_journal" }),
247
- Index3(["userAddress", "createdAt"])
262
+ Index3(["userAddress", "createdAt"]),
263
+ Index3(
264
+ "UQ_ledger_journal_user_token_tx_reason",
265
+ ["userAddress", "tokenAddress", "txHash", "reason"],
266
+ {
267
+ unique: true,
268
+ where: '"tx_hash" IS NOT NULL'
269
+ }
270
+ )
248
271
  ], LedgerJournalEntity);
249
272
 
250
273
  // src/postgresPointLedger.ts
274
+ var RETRIABLE_PG_CODES = /* @__PURE__ */ new Set(["40P01", "40001"]);
275
+ var UNIQUE_VIOLATION = "23505";
276
+ var JOURNAL_IDEMPOTENCY_CONSTRAINT = "UQ_ledger_journal_user_token_tx_reason";
277
+ var LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT = "UQ_ledger_journal_user_tx_reason";
278
+ function isRetriablePgError(err) {
279
+ const e = err;
280
+ if (!e) return false;
281
+ if (e.code && RETRIABLE_PG_CODES.has(e.code)) return true;
282
+ if (e.message && /deadlock detected|could not serialize/i.test(e.message)) {
283
+ return true;
284
+ }
285
+ return false;
286
+ }
287
+ function isJournalIdempotencyViolation(err) {
288
+ const e = err;
289
+ if (!e) return false;
290
+ const code = e.code ?? e.driverError?.code;
291
+ if (code !== UNIQUE_VIOLATION) return false;
292
+ const constraint = e.constraint ?? e.driverError?.constraint;
293
+ if (constraint === JOURNAL_IDEMPOTENCY_CONSTRAINT || constraint === LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT) {
294
+ return true;
295
+ }
296
+ const message = e.message ?? e.driverError?.message ?? "";
297
+ return message.includes(JOURNAL_IDEMPOTENCY_CONSTRAINT) || message.includes(LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT);
298
+ }
299
+ async function withDeadlockRetry(dataSource, fn, maxAttempts = 3) {
300
+ let attempt = 0;
301
+ let delayMs = 25;
302
+ for (; ; ) {
303
+ attempt++;
304
+ try {
305
+ return await dataSource.transaction(fn);
306
+ } catch (err) {
307
+ if (attempt >= maxAttempts || !isRetriablePgError(err)) {
308
+ throw err;
309
+ }
310
+ await new Promise(
311
+ (r) => setTimeout(r, delayMs + Math.floor(Math.random() * delayMs))
312
+ );
313
+ delayMs *= 2;
314
+ }
315
+ }
316
+ }
251
317
  var PostgresPointLedger = class {
252
318
  constructor(dataSource, options = {}) {
253
319
  this.dataSource = dataSource;
@@ -260,13 +326,47 @@ var PostgresPointLedger = class {
260
326
  // ---------------------------------------------------------------------
261
327
  async getBalance(userAddress, tokenAddress) {
262
328
  const { user, token } = normalize(userAddress, tokenAddress);
263
- await this.dataSource.getRepository(LockedMintEntity).createQueryBuilder().update().set({ status: "EXPIRED" }).where("status = :pending", { pending: "PENDING" }).andWhere("expires_at <= :now", { now: /* @__PURE__ */ new Date() }).execute();
264
- const balanceRow = await this.dataSource.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
329
+ const [balanceRow, locked] = await Promise.all([
330
+ this.dataSource.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } }),
331
+ this.sumPendingLocks(user, token)
332
+ ]);
265
333
  const total = balanceRow?.balance ?? 0n;
266
- const locked = await this.sumPendingLocks(user, token);
267
334
  const available = total - locked;
268
335
  return available < 0n ? 0n : available;
269
336
  }
337
+ /**
338
+ * Background sweep — marks all expired PENDING locks as EXPIRED in
339
+ * a single UPDATE. Issuers SHOULD call this periodically (e.g.
340
+ * every 1-5 minutes via a cron / NestJS `@Interval`) to keep the
341
+ * lock table from growing unbounded.
342
+ *
343
+ * A single sweep amortizes the write cost vs `getBalance` doing a
344
+ * wide UPDATE on every read. Returns the number of rows transitioned.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * import { Interval } from "@nestjs/schedule";
349
+ *
350
+ * @Injectable()
351
+ * export class LockSweepService {
352
+ * constructor(private readonly ledger: PostgresPointLedger) {}
353
+ *
354
+ * @Interval(5 * 60 * 1000) // 5 minutes
355
+ * async sweep() {
356
+ * const swept = await this.ledger.markExpiredLocks();
357
+ * this.logger.debug(`expired ${swept} mint locks`);
358
+ * }
359
+ * }
360
+ * ```
361
+ */
362
+ async markExpiredLocks() {
363
+ const result = await this.dataSource.getRepository(LockedMintEntity).createQueryBuilder().update().set({ status: "EXPIRED" }).where("status = :pending", { pending: "PENDING" }).andWhere("expires_at <= :now", { now: /* @__PURE__ */ new Date() }).execute();
364
+ const swept = result.affected ?? 0;
365
+ if (swept > 0) {
366
+ this.logger?.debug?.(`markExpiredLocks: swept ${swept} mint locks`);
367
+ }
368
+ return swept;
369
+ }
270
370
  async getLockedRequests(userAddress, tokenAddress) {
271
371
  const { user, token } = normalize(userAddress, tokenAddress);
272
372
  const rows = await this.dataSource.getRepository(LockedMintEntity).find({
@@ -332,8 +432,8 @@ var PostgresPointLedger = class {
332
432
  throw new Error("creditBalance: amount must be positive");
333
433
  }
334
434
  const { user, token } = normalize(userAddress, tokenAddress);
335
- await this.dataSource.transaction(async (tx) => {
336
- const existing = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
435
+ await withDeadlockRetry(this.dataSource, async (tx) => {
436
+ const existing = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
337
437
  const next = (existing?.balance ?? 0n) + amount;
338
438
  await tx.getRepository(UserBalanceEntity).upsert(
339
439
  { userAddress: user, tokenAddress: token, balance: next },
@@ -356,8 +456,8 @@ var PostgresPointLedger = class {
356
456
  throw new Error("lockForMinting: lockDurationMs must be positive");
357
457
  }
358
458
  const { user, token } = normalize(userAddress, tokenAddress);
359
- return this.dataSource.transaction(async (tx) => {
360
- const balanceRow = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
459
+ return withDeadlockRetry(this.dataSource, async (tx) => {
460
+ const balanceRow = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
361
461
  const total = balanceRow?.balance ?? 0n;
362
462
  const pendingTotal = await tx.getRepository(LockedMintEntity).createQueryBuilder("lock").select("COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)", "sum").where("lock.user_address = :user", { user }).andWhere("lock.token_address = :token", { token }).andWhere("lock.status = :pending", { pending: "PENDING" }).andWhere("lock.expires_at > :now", { now: /* @__PURE__ */ new Date() }).getRawOne();
363
463
  const locked = pendingTotal ? BigInt(pendingTotal.sum) : 0n;
@@ -391,43 +491,72 @@ var PostgresPointLedger = class {
391
491
  throw new Error("deductBalance: amount must be positive");
392
492
  }
393
493
  const { user, token } = normalize(userAddress, tokenAddress);
394
- await this.dataSource.transaction(async (tx) => {
395
- const balance = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
396
- if (!balance || balance.balance < amount) {
397
- throw new Error(
398
- `Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`
494
+ try {
495
+ await withDeadlockRetry(this.dataSource, async (tx) => {
496
+ const balance = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
497
+ const already = await tx.getRepository(LedgerJournalEntity).findOne({
498
+ where: {
499
+ userAddress: user,
500
+ tokenAddress: token,
501
+ txHash,
502
+ reason: "MINT_CONFIRMED"
503
+ }
504
+ });
505
+ if (already) {
506
+ this.logger?.debug?.(
507
+ `deductBalance: idempotent skip tx=${txHash} user=${user} token=${token}`
508
+ );
509
+ return;
510
+ }
511
+ if (!balance || balance.balance < amount) {
512
+ throw new Error(
513
+ `Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`
514
+ );
515
+ }
516
+ await tx.getRepository(UserBalanceEntity).update(
517
+ { userAddress: user, tokenAddress: token },
518
+ { balance: balance.balance - amount }
399
519
  );
400
- }
401
- await tx.getRepository(UserBalanceEntity).update(
402
- { userAddress: user, tokenAddress: token },
403
- { balance: balance.balance - amount }
404
- );
405
- await tx.getRepository(LedgerJournalEntity).insert({
406
- userAddress: user,
407
- tokenAddress: token,
408
- delta: -amount,
409
- reason: "MINT_CONFIRMED",
410
- txHash
411
- });
412
- const match = await tx.getRepository(LockedMintEntity).findOne({
413
- where: {
520
+ await tx.getRepository(LedgerJournalEntity).insert({
414
521
  userAddress: user,
415
522
  tokenAddress: token,
416
- amount,
417
- status: "PENDING"
418
- },
419
- order: { createdAt: "ASC" }
523
+ delta: -amount,
524
+ reason: "MINT_CONFIRMED",
525
+ txHash
526
+ });
527
+ const match = await tx.getRepository(LockedMintEntity).findOne({
528
+ where: {
529
+ userAddress: user,
530
+ tokenAddress: token,
531
+ amount,
532
+ status: "PENDING"
533
+ },
534
+ order: { createdAt: "ASC" }
535
+ });
536
+ if (match) {
537
+ await tx.getRepository(LockedMintEntity).update({ id: match.id }, { status: "MINTED", txHash });
538
+ }
420
539
  });
421
- if (match) {
422
- await tx.getRepository(LockedMintEntity).update({ id: match.id }, { status: "MINTED", txHash });
540
+ } catch (err) {
541
+ if (isJournalIdempotencyViolation(err)) {
542
+ this.logger?.debug?.(
543
+ `deductBalance: idempotent (concurrent race) tx=${txHash} user=${user}`
544
+ );
545
+ return;
423
546
  }
424
- });
547
+ throw err;
548
+ }
425
549
  this.logger?.log?.(`deduct ${user}[${token}] -${amount} tx=${txHash}`);
426
550
  }
427
551
  async updateMintStatus(lockId, status, txHash) {
428
552
  const update = { status };
429
553
  if (txHash) update.txHash = txHash;
430
- await this.dataSource.getRepository(LockedMintEntity).update({ id: lockId }, update);
554
+ const result = await this.dataSource.getRepository(LockedMintEntity).createQueryBuilder().update().set(update).where("id = :id", { id: lockId }).andWhere("status = :pending", { pending: "PENDING" }).execute();
555
+ if ((result.affected ?? 0) === 0) {
556
+ this.logger?.debug?.(
557
+ `updateMintStatus: lock ${lockId} not in PENDING (terminal state), no-op`
558
+ );
559
+ }
431
560
  }
432
561
  async bindMintUserOpHash(lockId, userOpHash) {
433
562
  await this.dataSource.getRepository(LockedMintEntity).update({ id: lockId }, { userOpHash });
@@ -459,8 +588,8 @@ var PostgresPointLedger = class {
459
588
  return row.id;
460
589
  }
461
590
  async resolveCreditByBurnTx(lockId, txHash) {
462
- await this.dataSource.transaction(async (tx) => {
463
- const credit = await tx.getRepository(PendingCreditEntity).findOne({ where: { id: lockId } });
591
+ const run = async () => withDeadlockRetry(this.dataSource, async (tx) => {
592
+ const credit = await tx.getRepository(PendingCreditEntity).createQueryBuilder("credit").setLock("pessimistic_write").where("credit.id = :id", { id: lockId }).getOne();
464
593
  if (!credit) {
465
594
  throw new Error(
466
595
  `resolveCreditByBurnTx: unknown pending credit ${lockId}`
@@ -494,7 +623,7 @@ var PostgresPointLedger = class {
494
623
  );
495
624
  return;
496
625
  }
497
- const balance = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
626
+ const balance = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
498
627
  const next = (balance?.balance ?? 0n) + credit.amount;
499
628
  await tx.getRepository(UserBalanceEntity).upsert(
500
629
  { userAddress: user, tokenAddress: token, balance: next },
@@ -512,6 +641,18 @@ var PostgresPointLedger = class {
512
641
  { status: "RESOLVED", txHash, resolvedAt: /* @__PURE__ */ new Date() }
513
642
  );
514
643
  });
644
+ try {
645
+ await run();
646
+ } catch (err) {
647
+ if (isJournalIdempotencyViolation(err)) {
648
+ this.logger?.debug?.(
649
+ `resolveCreditByBurnTx: concurrent race on tx=${txHash} lock=${lockId}, replaying via sibling defense`
650
+ );
651
+ await run();
652
+ } else {
653
+ throw err;
654
+ }
655
+ }
515
656
  this.logger?.log?.(`resolve pending credit ${lockId} tx=${txHash}`);
516
657
  }
517
658
  /**
@@ -620,13 +761,120 @@ var PostgresCursorStore = class _PostgresCursorStore {
620
761
  }
621
762
  };
622
763
 
764
+ // src/postgresRedemptionHistoryStore.ts
765
+ import { getAddress as getAddress2 } from "viem";
766
+
767
+ // src/entities/redemption-history.entity.ts
768
+ import {
769
+ Column as Column6,
770
+ CreateDateColumn as CreateDateColumn4,
771
+ Entity as Entity6,
772
+ Index as Index4,
773
+ PrimaryGeneratedColumn as PrimaryGeneratedColumn4
774
+ } from "typeorm";
775
+ var RedemptionHistoryEntity = class {
776
+ id;
777
+ userAddress;
778
+ tokenAddress;
779
+ amountPt;
780
+ createdAtUnixSec;
781
+ reservationId;
782
+ rowCreatedAt;
783
+ };
784
+ __decorateClass([
785
+ PrimaryGeneratedColumn4("uuid")
786
+ ], RedemptionHistoryEntity.prototype, "id", 2);
787
+ __decorateClass([
788
+ Column6({ name: "user_address", type: "varchar", length: 42 })
789
+ ], RedemptionHistoryEntity.prototype, "userAddress", 2);
790
+ __decorateClass([
791
+ Column6({ name: "token_address", type: "varchar", length: 42, nullable: true })
792
+ ], RedemptionHistoryEntity.prototype, "tokenAddress", 2);
793
+ __decorateClass([
794
+ Column6({
795
+ name: "amount_pt",
796
+ type: "numeric",
797
+ precision: 78,
798
+ scale: 0,
799
+ transformer: {
800
+ to: (value) => value.toString(),
801
+ from: (value) => BigInt(value)
802
+ }
803
+ })
804
+ ], RedemptionHistoryEntity.prototype, "amountPt", 2);
805
+ __decorateClass([
806
+ Column6({ name: "created_at_unix_sec", type: "bigint" })
807
+ ], RedemptionHistoryEntity.prototype, "createdAtUnixSec", 2);
808
+ __decorateClass([
809
+ Column6({
810
+ name: "reservation_id",
811
+ type: "varchar",
812
+ length: 64,
813
+ nullable: true
814
+ })
815
+ ], RedemptionHistoryEntity.prototype, "reservationId", 2);
816
+ __decorateClass([
817
+ CreateDateColumn4({ name: "row_created_at", type: "timestamptz" })
818
+ ], RedemptionHistoryEntity.prototype, "rowCreatedAt", 2);
819
+ RedemptionHistoryEntity = __decorateClass([
820
+ Entity6({ name: "redemption_history" }),
821
+ Index4("idx_redemption_history_user_created", [
822
+ "userAddress",
823
+ "createdAtUnixSec"
824
+ ])
825
+ ], RedemptionHistoryEntity);
826
+
827
+ // src/postgresRedemptionHistoryStore.ts
828
+ var PostgresRedemptionHistoryStore = class {
829
+ constructor(dataSource) {
830
+ this.dataSource = dataSource;
831
+ }
832
+ dataSource;
833
+ async sumRedeemedSince(user, sinceUnixSec, pointTokenAddress) {
834
+ const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
835
+ const qb = repo.createQueryBuilder("rh").select("COALESCE(SUM(rh.amount_pt), 0)", "sum").where("rh.user_address = :user", { user: getAddress2(user) }).andWhere("rh.created_at_unix_sec >= :since", { since: sinceUnixSec });
836
+ if (pointTokenAddress !== void 0) {
837
+ qb.andWhere("rh.token_address = :token", {
838
+ token: getAddress2(pointTokenAddress)
839
+ });
840
+ } else {
841
+ }
842
+ const row = await qb.getRawOne() ?? { sum: "0" };
843
+ return BigInt(row.sum ?? "0");
844
+ }
845
+ async getLastRedeemedAtUnixSec(user, pointTokenAddress) {
846
+ const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
847
+ const qb = repo.createQueryBuilder("rh").select("rh.created_at_unix_sec", "ts").where("rh.user_address = :user", { user: getAddress2(user) }).orderBy("rh.created_at_unix_sec", "DESC").limit(1);
848
+ if (pointTokenAddress !== void 0) {
849
+ qb.andWhere("rh.token_address = :token", {
850
+ token: getAddress2(pointTokenAddress)
851
+ });
852
+ }
853
+ const row = await qb.getRawOne();
854
+ if (!row || row.ts === null) return null;
855
+ return Number(row.ts);
856
+ }
857
+ async recordRedemption(entry) {
858
+ const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
859
+ const row = repo.create({
860
+ userAddress: getAddress2(entry.user),
861
+ tokenAddress: entry.pointTokenAddress ? getAddress2(entry.pointTokenAddress) : null,
862
+ amountPt: entry.amountPt,
863
+ createdAtUnixSec: String(entry.unixSec),
864
+ reservationId: entry.reservationId ?? null
865
+ });
866
+ await repo.save(row);
867
+ }
868
+ };
869
+
623
870
  // src/entities/index.ts
624
871
  var PAFI_ENTITIES = [
625
872
  LockedMintEntity,
626
873
  PendingCreditEntity,
627
874
  UserBalanceEntity,
628
875
  LedgerJournalEntity,
629
- IndexerCursorEntity
876
+ IndexerCursorEntity,
877
+ RedemptionHistoryEntity
630
878
  ];
631
879
 
632
880
  // src/migrations/1700000000000-InitialSchema.ts
@@ -732,9 +980,135 @@ var InitialSchema1700000000000 = class {
732
980
  }
733
981
  };
734
982
 
983
+ // src/migrations/1746230400001-CreateRedemptionHistory.ts
984
+ var CreateRedemptionHistory1746230400001 = class {
985
+ name = "CreateRedemptionHistory1746230400001";
986
+ async up(queryRunner) {
987
+ await queryRunner.query(`
988
+ CREATE TABLE IF NOT EXISTS redemption_history (
989
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
990
+ user_address varchar(42) NOT NULL,
991
+ token_address varchar(42),
992
+ amount_pt numeric(78, 0) NOT NULL,
993
+ created_at_unix_sec bigint NOT NULL,
994
+ reservation_id varchar(64),
995
+ row_created_at timestamptz NOT NULL DEFAULT NOW(),
996
+ CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)
997
+ )
998
+ `);
999
+ await queryRunner.query(`
1000
+ CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created
1001
+ ON redemption_history (user_address, created_at_unix_sec DESC)
1002
+ `);
1003
+ }
1004
+ async down(queryRunner) {
1005
+ await queryRunner.query(
1006
+ `DROP INDEX IF EXISTS idx_redemption_history_user_created`
1007
+ );
1008
+ await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);
1009
+ }
1010
+ };
1011
+
1012
+ // src/migrations/1747500000000-AddJournalIdempotencyIndex.ts
1013
+ var AddJournalIdempotencyIndex1747500000000 = class {
1014
+ name = "AddJournalIdempotencyIndex1747500000000";
1015
+ async up(queryRunner) {
1016
+ await queryRunner.query(`
1017
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
1018
+ ON "ledger_journal" ("user_address", "tx_hash", "reason")
1019
+ WHERE "tx_hash" IS NOT NULL
1020
+ `);
1021
+ }
1022
+ async down(queryRunner) {
1023
+ await queryRunner.query(
1024
+ `DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
1025
+ );
1026
+ }
1027
+ };
1028
+
1029
+ // src/migrations/1747600000000-AddLockedMintCompositeIndexes.ts
1030
+ var AddLockedMintCompositeIndexes1747600000000 = class {
1031
+ name = "AddLockedMintCompositeIndexes1747600000000";
1032
+ /**
1033
+ * CONCURRENTLY index DDL cannot run inside a transaction. Tell
1034
+ * TypeORM to issue these statements directly.
1035
+ */
1036
+ transaction = false;
1037
+ async up(queryRunner) {
1038
+ await queryRunner.query(`
1039
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
1040
+ "IDX_locked_mint_user_token_status_expires"
1041
+ ON "locked_mint_requests"
1042
+ ("user_address", "token_address", "status", "expires_at")
1043
+ `);
1044
+ await queryRunner.query(`
1045
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
1046
+ "IDX_locked_mint_user_token_amount_status"
1047
+ ON "locked_mint_requests"
1048
+ ("user_address", "token_address", "amount", "status")
1049
+ `);
1050
+ await queryRunner.query(`
1051
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
1052
+ "IDX_locked_mint_pending_expires"
1053
+ ON "locked_mint_requests" ("expires_at")
1054
+ WHERE "status" = 'PENDING'
1055
+ `);
1056
+ await queryRunner.query(
1057
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_status"`
1058
+ );
1059
+ }
1060
+ async down(queryRunner) {
1061
+ await queryRunner.query(`
1062
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_locked_mint_user_status"
1063
+ ON "locked_mint_requests" ("user_address", "token_address", "status")
1064
+ `);
1065
+ await queryRunner.query(
1066
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_pending_expires"`
1067
+ );
1068
+ await queryRunner.query(
1069
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_amount_status"`
1070
+ );
1071
+ await queryRunner.query(
1072
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_status_expires"`
1073
+ );
1074
+ }
1075
+ };
1076
+
1077
+ // src/migrations/1747700000000-FixIdempotencyAddTokenAddress.ts
1078
+ var FixIdempotencyAddTokenAddress1747700000000 = class {
1079
+ name = "FixIdempotencyAddTokenAddress1747700000000";
1080
+ async up(queryRunner) {
1081
+ await queryRunner.query(
1082
+ `DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
1083
+ );
1084
+ await queryRunner.query(`
1085
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_token_tx_reason"
1086
+ ON "ledger_journal" ("user_address", "token_address", "tx_hash", "reason")
1087
+ WHERE "tx_hash" IS NOT NULL
1088
+ `);
1089
+ }
1090
+ async down(queryRunner) {
1091
+ await queryRunner.query(
1092
+ `DROP INDEX IF EXISTS "UQ_ledger_journal_user_token_tx_reason"`
1093
+ );
1094
+ await queryRunner.query(`
1095
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
1096
+ ON "ledger_journal" ("user_address", "tx_hash", "reason")
1097
+ WHERE "tx_hash" IS NOT NULL
1098
+ `);
1099
+ }
1100
+ };
1101
+
735
1102
  // src/migrations/index.ts
736
- var PAFI_MIGRATIONS = [InitialSchema1700000000000];
1103
+ var PAFI_MIGRATIONS = [
1104
+ InitialSchema1700000000000,
1105
+ CreateRedemptionHistory1746230400001,
1106
+ AddJournalIdempotencyIndex1747500000000,
1107
+ AddLockedMintCompositeIndexes1747600000000,
1108
+ FixIdempotencyAddTokenAddress1747700000000
1109
+ ];
737
1110
  export {
1111
+ CreateRedemptionHistory1746230400001,
738
1112
  IndexerCursorEntity,
739
1113
  InitialSchema1700000000000,
740
1114
  LedgerJournalEntity,
@@ -744,6 +1118,8 @@ export {
744
1118
  PendingCreditEntity,
745
1119
  PostgresCursorStore,
746
1120
  PostgresPointLedger,
1121
+ PostgresRedemptionHistoryStore,
1122
+ RedemptionHistoryEntity,
747
1123
  UserBalanceEntity
748
1124
  };
749
1125
  //# sourceMappingURL=index.js.map