@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/README.md +174 -3
- package/dist/entities/index.cjs +83 -3
- package/dist/entities/index.cjs.map +1 -1
- package/dist/entities/index.d.cts +72 -4
- package/dist/entities/index.d.ts +72 -4
- package/dist/entities/index.js +88 -3
- package/dist/entities/index.js.map +1 -1
- package/dist/index.cjs +414 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -5
- package/dist/index.d.ts +54 -5
- package/dist/index.js +417 -41
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +134 -1
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +151 -6
- package/dist/migrations/index.d.ts +151 -6
- package/dist/migrations/index.js +130 -1
- package/dist/migrations/index.js.map +1 -1
- package/package.json +16 -5
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(
|
|
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
|
-
|
|
264
|
-
|
|
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
|
|
336
|
-
const existing = await tx.getRepository(UserBalanceEntity).
|
|
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
|
|
360
|
-
const balanceRow = await tx.getRepository(UserBalanceEntity).
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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 },
|
|
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
|
-
|
|
463
|
-
const credit = await tx.getRepository(PendingCreditEntity).
|
|
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).
|
|
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 = [
|
|
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
|