@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.cjs
CHANGED
|
@@ -28,6 +28,7 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
28
28
|
// src/index.ts
|
|
29
29
|
var src_exports = {};
|
|
30
30
|
__export(src_exports, {
|
|
31
|
+
CreateRedemptionHistory1746230400001: () => CreateRedemptionHistory1746230400001,
|
|
31
32
|
IndexerCursorEntity: () => IndexerCursorEntity,
|
|
32
33
|
InitialSchema1700000000000: () => InitialSchema1700000000000,
|
|
33
34
|
LedgerJournalEntity: () => LedgerJournalEntity,
|
|
@@ -37,6 +38,8 @@ __export(src_exports, {
|
|
|
37
38
|
PendingCreditEntity: () => PendingCreditEntity,
|
|
38
39
|
PostgresCursorStore: () => PostgresCursorStore,
|
|
39
40
|
PostgresPointLedger: () => PostgresPointLedger,
|
|
41
|
+
PostgresRedemptionHistoryStore: () => PostgresRedemptionHistoryStore,
|
|
42
|
+
RedemptionHistoryEntity: () => RedemptionHistoryEntity,
|
|
40
43
|
UserBalanceEntity: () => UserBalanceEntity
|
|
41
44
|
});
|
|
42
45
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -105,7 +108,22 @@ __decorateClass([
|
|
|
105
108
|
], LockedMintEntity.prototype, "userOpHash", 2);
|
|
106
109
|
LockedMintEntity = __decorateClass([
|
|
107
110
|
(0, import_typeorm.Entity)({ name: "locked_mint_requests" }),
|
|
108
|
-
(0, import_typeorm.Index)(
|
|
111
|
+
(0, import_typeorm.Index)("IDX_locked_mint_user_token_status_expires", [
|
|
112
|
+
"userAddress",
|
|
113
|
+
"tokenAddress",
|
|
114
|
+
"status",
|
|
115
|
+
"expiresAt"
|
|
116
|
+
]),
|
|
117
|
+
(0, import_typeorm.Index)("IDX_locked_mint_user_token_amount_status", [
|
|
118
|
+
"userAddress",
|
|
119
|
+
"tokenAddress",
|
|
120
|
+
"amount",
|
|
121
|
+
"status"
|
|
122
|
+
]),
|
|
123
|
+
(0, import_typeorm.Index)("IDX_locked_mint_pending_expires", ["expiresAt"], {
|
|
124
|
+
where: `"status" = 'PENDING'`
|
|
125
|
+
}),
|
|
126
|
+
(0, import_typeorm.Index)("IDX_locked_mint_user_op_hash", ["userOpHash"])
|
|
109
127
|
], LockedMintEntity);
|
|
110
128
|
|
|
111
129
|
// src/entities/pending-credit.entity.ts
|
|
@@ -258,10 +276,61 @@ __decorateClass([
|
|
|
258
276
|
], LedgerJournalEntity.prototype, "createdAt", 2);
|
|
259
277
|
LedgerJournalEntity = __decorateClass([
|
|
260
278
|
(0, import_typeorm4.Entity)({ name: "ledger_journal" }),
|
|
261
|
-
(0, import_typeorm4.Index)(["userAddress", "createdAt"])
|
|
279
|
+
(0, import_typeorm4.Index)(["userAddress", "createdAt"]),
|
|
280
|
+
(0, import_typeorm4.Index)(
|
|
281
|
+
"UQ_ledger_journal_user_token_tx_reason",
|
|
282
|
+
["userAddress", "tokenAddress", "txHash", "reason"],
|
|
283
|
+
{
|
|
284
|
+
unique: true,
|
|
285
|
+
where: '"tx_hash" IS NOT NULL'
|
|
286
|
+
}
|
|
287
|
+
)
|
|
262
288
|
], LedgerJournalEntity);
|
|
263
289
|
|
|
264
290
|
// src/postgresPointLedger.ts
|
|
291
|
+
var RETRIABLE_PG_CODES = /* @__PURE__ */ new Set(["40P01", "40001"]);
|
|
292
|
+
var UNIQUE_VIOLATION = "23505";
|
|
293
|
+
var JOURNAL_IDEMPOTENCY_CONSTRAINT = "UQ_ledger_journal_user_token_tx_reason";
|
|
294
|
+
var LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT = "UQ_ledger_journal_user_tx_reason";
|
|
295
|
+
function isRetriablePgError(err) {
|
|
296
|
+
const e = err;
|
|
297
|
+
if (!e) return false;
|
|
298
|
+
if (e.code && RETRIABLE_PG_CODES.has(e.code)) return true;
|
|
299
|
+
if (e.message && /deadlock detected|could not serialize/i.test(e.message)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
function isJournalIdempotencyViolation(err) {
|
|
305
|
+
const e = err;
|
|
306
|
+
if (!e) return false;
|
|
307
|
+
const code = e.code ?? e.driverError?.code;
|
|
308
|
+
if (code !== UNIQUE_VIOLATION) return false;
|
|
309
|
+
const constraint = e.constraint ?? e.driverError?.constraint;
|
|
310
|
+
if (constraint === JOURNAL_IDEMPOTENCY_CONSTRAINT || constraint === LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
const message = e.message ?? e.driverError?.message ?? "";
|
|
314
|
+
return message.includes(JOURNAL_IDEMPOTENCY_CONSTRAINT) || message.includes(LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT);
|
|
315
|
+
}
|
|
316
|
+
async function withDeadlockRetry(dataSource, fn, maxAttempts = 3) {
|
|
317
|
+
let attempt = 0;
|
|
318
|
+
let delayMs = 25;
|
|
319
|
+
for (; ; ) {
|
|
320
|
+
attempt++;
|
|
321
|
+
try {
|
|
322
|
+
return await dataSource.transaction(fn);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (attempt >= maxAttempts || !isRetriablePgError(err)) {
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
await new Promise(
|
|
328
|
+
(r) => setTimeout(r, delayMs + Math.floor(Math.random() * delayMs))
|
|
329
|
+
);
|
|
330
|
+
delayMs *= 2;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
265
334
|
var PostgresPointLedger = class {
|
|
266
335
|
constructor(dataSource, options = {}) {
|
|
267
336
|
this.dataSource = dataSource;
|
|
@@ -274,13 +343,47 @@ var PostgresPointLedger = class {
|
|
|
274
343
|
// ---------------------------------------------------------------------
|
|
275
344
|
async getBalance(userAddress, tokenAddress) {
|
|
276
345
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
277
|
-
|
|
278
|
-
|
|
346
|
+
const [balanceRow, locked] = await Promise.all([
|
|
347
|
+
this.dataSource.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } }),
|
|
348
|
+
this.sumPendingLocks(user, token)
|
|
349
|
+
]);
|
|
279
350
|
const total = balanceRow?.balance ?? 0n;
|
|
280
|
-
const locked = await this.sumPendingLocks(user, token);
|
|
281
351
|
const available = total - locked;
|
|
282
352
|
return available < 0n ? 0n : available;
|
|
283
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Background sweep — marks all expired PENDING locks as EXPIRED in
|
|
356
|
+
* a single UPDATE. Issuers SHOULD call this periodically (e.g.
|
|
357
|
+
* every 1-5 minutes via a cron / NestJS `@Interval`) to keep the
|
|
358
|
+
* lock table from growing unbounded.
|
|
359
|
+
*
|
|
360
|
+
* A single sweep amortizes the write cost vs `getBalance` doing a
|
|
361
|
+
* wide UPDATE on every read. Returns the number of rows transitioned.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```ts
|
|
365
|
+
* import { Interval } from "@nestjs/schedule";
|
|
366
|
+
*
|
|
367
|
+
* @Injectable()
|
|
368
|
+
* export class LockSweepService {
|
|
369
|
+
* constructor(private readonly ledger: PostgresPointLedger) {}
|
|
370
|
+
*
|
|
371
|
+
* @Interval(5 * 60 * 1000) // 5 minutes
|
|
372
|
+
* async sweep() {
|
|
373
|
+
* const swept = await this.ledger.markExpiredLocks();
|
|
374
|
+
* this.logger.debug(`expired ${swept} mint locks`);
|
|
375
|
+
* }
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
async markExpiredLocks() {
|
|
380
|
+
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();
|
|
381
|
+
const swept = result.affected ?? 0;
|
|
382
|
+
if (swept > 0) {
|
|
383
|
+
this.logger?.debug?.(`markExpiredLocks: swept ${swept} mint locks`);
|
|
384
|
+
}
|
|
385
|
+
return swept;
|
|
386
|
+
}
|
|
284
387
|
async getLockedRequests(userAddress, tokenAddress) {
|
|
285
388
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
286
389
|
const rows = await this.dataSource.getRepository(LockedMintEntity).find({
|
|
@@ -346,8 +449,8 @@ var PostgresPointLedger = class {
|
|
|
346
449
|
throw new Error("creditBalance: amount must be positive");
|
|
347
450
|
}
|
|
348
451
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
349
|
-
await this.dataSource
|
|
350
|
-
const existing = await tx.getRepository(UserBalanceEntity).
|
|
452
|
+
await withDeadlockRetry(this.dataSource, async (tx) => {
|
|
453
|
+
const existing = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
|
|
351
454
|
const next = (existing?.balance ?? 0n) + amount;
|
|
352
455
|
await tx.getRepository(UserBalanceEntity).upsert(
|
|
353
456
|
{ userAddress: user, tokenAddress: token, balance: next },
|
|
@@ -370,8 +473,8 @@ var PostgresPointLedger = class {
|
|
|
370
473
|
throw new Error("lockForMinting: lockDurationMs must be positive");
|
|
371
474
|
}
|
|
372
475
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
373
|
-
return this.dataSource
|
|
374
|
-
const balanceRow = await tx.getRepository(UserBalanceEntity).
|
|
476
|
+
return withDeadlockRetry(this.dataSource, async (tx) => {
|
|
477
|
+
const balanceRow = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
|
|
375
478
|
const total = balanceRow?.balance ?? 0n;
|
|
376
479
|
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();
|
|
377
480
|
const locked = pendingTotal ? BigInt(pendingTotal.sum) : 0n;
|
|
@@ -405,43 +508,72 @@ var PostgresPointLedger = class {
|
|
|
405
508
|
throw new Error("deductBalance: amount must be positive");
|
|
406
509
|
}
|
|
407
510
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
511
|
+
try {
|
|
512
|
+
await withDeadlockRetry(this.dataSource, async (tx) => {
|
|
513
|
+
const balance = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
|
|
514
|
+
const already = await tx.getRepository(LedgerJournalEntity).findOne({
|
|
515
|
+
where: {
|
|
516
|
+
userAddress: user,
|
|
517
|
+
tokenAddress: token,
|
|
518
|
+
txHash,
|
|
519
|
+
reason: "MINT_CONFIRMED"
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
if (already) {
|
|
523
|
+
this.logger?.debug?.(
|
|
524
|
+
`deductBalance: idempotent skip tx=${txHash} user=${user} token=${token}`
|
|
525
|
+
);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!balance || balance.balance < amount) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
await tx.getRepository(UserBalanceEntity).update(
|
|
534
|
+
{ userAddress: user, tokenAddress: token },
|
|
535
|
+
{ balance: balance.balance - amount }
|
|
413
536
|
);
|
|
414
|
-
|
|
415
|
-
await tx.getRepository(UserBalanceEntity).update(
|
|
416
|
-
{ userAddress: user, tokenAddress: token },
|
|
417
|
-
{ balance: balance.balance - amount }
|
|
418
|
-
);
|
|
419
|
-
await tx.getRepository(LedgerJournalEntity).insert({
|
|
420
|
-
userAddress: user,
|
|
421
|
-
tokenAddress: token,
|
|
422
|
-
delta: -amount,
|
|
423
|
-
reason: "MINT_CONFIRMED",
|
|
424
|
-
txHash
|
|
425
|
-
});
|
|
426
|
-
const match = await tx.getRepository(LockedMintEntity).findOne({
|
|
427
|
-
where: {
|
|
537
|
+
await tx.getRepository(LedgerJournalEntity).insert({
|
|
428
538
|
userAddress: user,
|
|
429
539
|
tokenAddress: token,
|
|
430
|
-
amount,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
540
|
+
delta: -amount,
|
|
541
|
+
reason: "MINT_CONFIRMED",
|
|
542
|
+
txHash
|
|
543
|
+
});
|
|
544
|
+
const match = await tx.getRepository(LockedMintEntity).findOne({
|
|
545
|
+
where: {
|
|
546
|
+
userAddress: user,
|
|
547
|
+
tokenAddress: token,
|
|
548
|
+
amount,
|
|
549
|
+
status: "PENDING"
|
|
550
|
+
},
|
|
551
|
+
order: { createdAt: "ASC" }
|
|
552
|
+
});
|
|
553
|
+
if (match) {
|
|
554
|
+
await tx.getRepository(LockedMintEntity).update({ id: match.id }, { status: "MINTED", txHash });
|
|
555
|
+
}
|
|
434
556
|
});
|
|
435
|
-
|
|
436
|
-
|
|
557
|
+
} catch (err) {
|
|
558
|
+
if (isJournalIdempotencyViolation(err)) {
|
|
559
|
+
this.logger?.debug?.(
|
|
560
|
+
`deductBalance: idempotent (concurrent race) tx=${txHash} user=${user}`
|
|
561
|
+
);
|
|
562
|
+
return;
|
|
437
563
|
}
|
|
438
|
-
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
439
566
|
this.logger?.log?.(`deduct ${user}[${token}] -${amount} tx=${txHash}`);
|
|
440
567
|
}
|
|
441
568
|
async updateMintStatus(lockId, status, txHash) {
|
|
442
569
|
const update = { status };
|
|
443
570
|
if (txHash) update.txHash = txHash;
|
|
444
|
-
await this.dataSource.getRepository(LockedMintEntity).update({ id: lockId },
|
|
571
|
+
const result = await this.dataSource.getRepository(LockedMintEntity).createQueryBuilder().update().set(update).where("id = :id", { id: lockId }).andWhere("status = :pending", { pending: "PENDING" }).execute();
|
|
572
|
+
if ((result.affected ?? 0) === 0) {
|
|
573
|
+
this.logger?.debug?.(
|
|
574
|
+
`updateMintStatus: lock ${lockId} not in PENDING (terminal state), no-op`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
445
577
|
}
|
|
446
578
|
async bindMintUserOpHash(lockId, userOpHash) {
|
|
447
579
|
await this.dataSource.getRepository(LockedMintEntity).update({ id: lockId }, { userOpHash });
|
|
@@ -473,8 +605,8 @@ var PostgresPointLedger = class {
|
|
|
473
605
|
return row.id;
|
|
474
606
|
}
|
|
475
607
|
async resolveCreditByBurnTx(lockId, txHash) {
|
|
476
|
-
|
|
477
|
-
const credit = await tx.getRepository(PendingCreditEntity).
|
|
608
|
+
const run = async () => withDeadlockRetry(this.dataSource, async (tx) => {
|
|
609
|
+
const credit = await tx.getRepository(PendingCreditEntity).createQueryBuilder("credit").setLock("pessimistic_write").where("credit.id = :id", { id: lockId }).getOne();
|
|
478
610
|
if (!credit) {
|
|
479
611
|
throw new Error(
|
|
480
612
|
`resolveCreditByBurnTx: unknown pending credit ${lockId}`
|
|
@@ -508,7 +640,7 @@ var PostgresPointLedger = class {
|
|
|
508
640
|
);
|
|
509
641
|
return;
|
|
510
642
|
}
|
|
511
|
-
const balance = await tx.getRepository(UserBalanceEntity).
|
|
643
|
+
const balance = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
|
|
512
644
|
const next = (balance?.balance ?? 0n) + credit.amount;
|
|
513
645
|
await tx.getRepository(UserBalanceEntity).upsert(
|
|
514
646
|
{ userAddress: user, tokenAddress: token, balance: next },
|
|
@@ -526,6 +658,18 @@ var PostgresPointLedger = class {
|
|
|
526
658
|
{ status: "RESOLVED", txHash, resolvedAt: /* @__PURE__ */ new Date() }
|
|
527
659
|
);
|
|
528
660
|
});
|
|
661
|
+
try {
|
|
662
|
+
await run();
|
|
663
|
+
} catch (err) {
|
|
664
|
+
if (isJournalIdempotencyViolation(err)) {
|
|
665
|
+
this.logger?.debug?.(
|
|
666
|
+
`resolveCreditByBurnTx: concurrent race on tx=${txHash} lock=${lockId}, replaying via sibling defense`
|
|
667
|
+
);
|
|
668
|
+
await run();
|
|
669
|
+
} else {
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
529
673
|
this.logger?.log?.(`resolve pending credit ${lockId} tx=${txHash}`);
|
|
530
674
|
}
|
|
531
675
|
/**
|
|
@@ -634,13 +778,114 @@ var PostgresCursorStore = class _PostgresCursorStore {
|
|
|
634
778
|
}
|
|
635
779
|
};
|
|
636
780
|
|
|
781
|
+
// src/postgresRedemptionHistoryStore.ts
|
|
782
|
+
var import_viem2 = require("viem");
|
|
783
|
+
|
|
784
|
+
// src/entities/redemption-history.entity.ts
|
|
785
|
+
var import_typeorm6 = require("typeorm");
|
|
786
|
+
var RedemptionHistoryEntity = class {
|
|
787
|
+
id;
|
|
788
|
+
userAddress;
|
|
789
|
+
tokenAddress;
|
|
790
|
+
amountPt;
|
|
791
|
+
createdAtUnixSec;
|
|
792
|
+
reservationId;
|
|
793
|
+
rowCreatedAt;
|
|
794
|
+
};
|
|
795
|
+
__decorateClass([
|
|
796
|
+
(0, import_typeorm6.PrimaryGeneratedColumn)("uuid")
|
|
797
|
+
], RedemptionHistoryEntity.prototype, "id", 2);
|
|
798
|
+
__decorateClass([
|
|
799
|
+
(0, import_typeorm6.Column)({ name: "user_address", type: "varchar", length: 42 })
|
|
800
|
+
], RedemptionHistoryEntity.prototype, "userAddress", 2);
|
|
801
|
+
__decorateClass([
|
|
802
|
+
(0, import_typeorm6.Column)({ name: "token_address", type: "varchar", length: 42, nullable: true })
|
|
803
|
+
], RedemptionHistoryEntity.prototype, "tokenAddress", 2);
|
|
804
|
+
__decorateClass([
|
|
805
|
+
(0, import_typeorm6.Column)({
|
|
806
|
+
name: "amount_pt",
|
|
807
|
+
type: "numeric",
|
|
808
|
+
precision: 78,
|
|
809
|
+
scale: 0,
|
|
810
|
+
transformer: {
|
|
811
|
+
to: (value) => value.toString(),
|
|
812
|
+
from: (value) => BigInt(value)
|
|
813
|
+
}
|
|
814
|
+
})
|
|
815
|
+
], RedemptionHistoryEntity.prototype, "amountPt", 2);
|
|
816
|
+
__decorateClass([
|
|
817
|
+
(0, import_typeorm6.Column)({ name: "created_at_unix_sec", type: "bigint" })
|
|
818
|
+
], RedemptionHistoryEntity.prototype, "createdAtUnixSec", 2);
|
|
819
|
+
__decorateClass([
|
|
820
|
+
(0, import_typeorm6.Column)({
|
|
821
|
+
name: "reservation_id",
|
|
822
|
+
type: "varchar",
|
|
823
|
+
length: 64,
|
|
824
|
+
nullable: true
|
|
825
|
+
})
|
|
826
|
+
], RedemptionHistoryEntity.prototype, "reservationId", 2);
|
|
827
|
+
__decorateClass([
|
|
828
|
+
(0, import_typeorm6.CreateDateColumn)({ name: "row_created_at", type: "timestamptz" })
|
|
829
|
+
], RedemptionHistoryEntity.prototype, "rowCreatedAt", 2);
|
|
830
|
+
RedemptionHistoryEntity = __decorateClass([
|
|
831
|
+
(0, import_typeorm6.Entity)({ name: "redemption_history" }),
|
|
832
|
+
(0, import_typeorm6.Index)("idx_redemption_history_user_created", [
|
|
833
|
+
"userAddress",
|
|
834
|
+
"createdAtUnixSec"
|
|
835
|
+
])
|
|
836
|
+
], RedemptionHistoryEntity);
|
|
837
|
+
|
|
838
|
+
// src/postgresRedemptionHistoryStore.ts
|
|
839
|
+
var PostgresRedemptionHistoryStore = class {
|
|
840
|
+
constructor(dataSource) {
|
|
841
|
+
this.dataSource = dataSource;
|
|
842
|
+
}
|
|
843
|
+
dataSource;
|
|
844
|
+
async sumRedeemedSince(user, sinceUnixSec, pointTokenAddress) {
|
|
845
|
+
const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
|
|
846
|
+
const qb = repo.createQueryBuilder("rh").select("COALESCE(SUM(rh.amount_pt), 0)", "sum").where("rh.user_address = :user", { user: (0, import_viem2.getAddress)(user) }).andWhere("rh.created_at_unix_sec >= :since", { since: sinceUnixSec });
|
|
847
|
+
if (pointTokenAddress !== void 0) {
|
|
848
|
+
qb.andWhere("rh.token_address = :token", {
|
|
849
|
+
token: (0, import_viem2.getAddress)(pointTokenAddress)
|
|
850
|
+
});
|
|
851
|
+
} else {
|
|
852
|
+
}
|
|
853
|
+
const row = await qb.getRawOne() ?? { sum: "0" };
|
|
854
|
+
return BigInt(row.sum ?? "0");
|
|
855
|
+
}
|
|
856
|
+
async getLastRedeemedAtUnixSec(user, pointTokenAddress) {
|
|
857
|
+
const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
|
|
858
|
+
const qb = repo.createQueryBuilder("rh").select("rh.created_at_unix_sec", "ts").where("rh.user_address = :user", { user: (0, import_viem2.getAddress)(user) }).orderBy("rh.created_at_unix_sec", "DESC").limit(1);
|
|
859
|
+
if (pointTokenAddress !== void 0) {
|
|
860
|
+
qb.andWhere("rh.token_address = :token", {
|
|
861
|
+
token: (0, import_viem2.getAddress)(pointTokenAddress)
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
const row = await qb.getRawOne();
|
|
865
|
+
if (!row || row.ts === null) return null;
|
|
866
|
+
return Number(row.ts);
|
|
867
|
+
}
|
|
868
|
+
async recordRedemption(entry) {
|
|
869
|
+
const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
|
|
870
|
+
const row = repo.create({
|
|
871
|
+
userAddress: (0, import_viem2.getAddress)(entry.user),
|
|
872
|
+
tokenAddress: entry.pointTokenAddress ? (0, import_viem2.getAddress)(entry.pointTokenAddress) : null,
|
|
873
|
+
amountPt: entry.amountPt,
|
|
874
|
+
createdAtUnixSec: String(entry.unixSec),
|
|
875
|
+
reservationId: entry.reservationId ?? null
|
|
876
|
+
});
|
|
877
|
+
await repo.save(row);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
637
881
|
// src/entities/index.ts
|
|
638
882
|
var PAFI_ENTITIES = [
|
|
639
883
|
LockedMintEntity,
|
|
640
884
|
PendingCreditEntity,
|
|
641
885
|
UserBalanceEntity,
|
|
642
886
|
LedgerJournalEntity,
|
|
643
|
-
IndexerCursorEntity
|
|
887
|
+
IndexerCursorEntity,
|
|
888
|
+
RedemptionHistoryEntity
|
|
644
889
|
];
|
|
645
890
|
|
|
646
891
|
// src/migrations/1700000000000-InitialSchema.ts
|
|
@@ -746,10 +991,136 @@ var InitialSchema1700000000000 = class {
|
|
|
746
991
|
}
|
|
747
992
|
};
|
|
748
993
|
|
|
994
|
+
// src/migrations/1746230400001-CreateRedemptionHistory.ts
|
|
995
|
+
var CreateRedemptionHistory1746230400001 = class {
|
|
996
|
+
name = "CreateRedemptionHistory1746230400001";
|
|
997
|
+
async up(queryRunner) {
|
|
998
|
+
await queryRunner.query(`
|
|
999
|
+
CREATE TABLE IF NOT EXISTS redemption_history (
|
|
1000
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1001
|
+
user_address varchar(42) NOT NULL,
|
|
1002
|
+
token_address varchar(42),
|
|
1003
|
+
amount_pt numeric(78, 0) NOT NULL,
|
|
1004
|
+
created_at_unix_sec bigint NOT NULL,
|
|
1005
|
+
reservation_id varchar(64),
|
|
1006
|
+
row_created_at timestamptz NOT NULL DEFAULT NOW(),
|
|
1007
|
+
CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)
|
|
1008
|
+
)
|
|
1009
|
+
`);
|
|
1010
|
+
await queryRunner.query(`
|
|
1011
|
+
CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created
|
|
1012
|
+
ON redemption_history (user_address, created_at_unix_sec DESC)
|
|
1013
|
+
`);
|
|
1014
|
+
}
|
|
1015
|
+
async down(queryRunner) {
|
|
1016
|
+
await queryRunner.query(
|
|
1017
|
+
`DROP INDEX IF EXISTS idx_redemption_history_user_created`
|
|
1018
|
+
);
|
|
1019
|
+
await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// src/migrations/1747500000000-AddJournalIdempotencyIndex.ts
|
|
1024
|
+
var AddJournalIdempotencyIndex1747500000000 = class {
|
|
1025
|
+
name = "AddJournalIdempotencyIndex1747500000000";
|
|
1026
|
+
async up(queryRunner) {
|
|
1027
|
+
await queryRunner.query(`
|
|
1028
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
|
|
1029
|
+
ON "ledger_journal" ("user_address", "tx_hash", "reason")
|
|
1030
|
+
WHERE "tx_hash" IS NOT NULL
|
|
1031
|
+
`);
|
|
1032
|
+
}
|
|
1033
|
+
async down(queryRunner) {
|
|
1034
|
+
await queryRunner.query(
|
|
1035
|
+
`DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/migrations/1747600000000-AddLockedMintCompositeIndexes.ts
|
|
1041
|
+
var AddLockedMintCompositeIndexes1747600000000 = class {
|
|
1042
|
+
name = "AddLockedMintCompositeIndexes1747600000000";
|
|
1043
|
+
/**
|
|
1044
|
+
* CONCURRENTLY index DDL cannot run inside a transaction. Tell
|
|
1045
|
+
* TypeORM to issue these statements directly.
|
|
1046
|
+
*/
|
|
1047
|
+
transaction = false;
|
|
1048
|
+
async up(queryRunner) {
|
|
1049
|
+
await queryRunner.query(`
|
|
1050
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
1051
|
+
"IDX_locked_mint_user_token_status_expires"
|
|
1052
|
+
ON "locked_mint_requests"
|
|
1053
|
+
("user_address", "token_address", "status", "expires_at")
|
|
1054
|
+
`);
|
|
1055
|
+
await queryRunner.query(`
|
|
1056
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
1057
|
+
"IDX_locked_mint_user_token_amount_status"
|
|
1058
|
+
ON "locked_mint_requests"
|
|
1059
|
+
("user_address", "token_address", "amount", "status")
|
|
1060
|
+
`);
|
|
1061
|
+
await queryRunner.query(`
|
|
1062
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
1063
|
+
"IDX_locked_mint_pending_expires"
|
|
1064
|
+
ON "locked_mint_requests" ("expires_at")
|
|
1065
|
+
WHERE "status" = 'PENDING'
|
|
1066
|
+
`);
|
|
1067
|
+
await queryRunner.query(
|
|
1068
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_status"`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
async down(queryRunner) {
|
|
1072
|
+
await queryRunner.query(`
|
|
1073
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_locked_mint_user_status"
|
|
1074
|
+
ON "locked_mint_requests" ("user_address", "token_address", "status")
|
|
1075
|
+
`);
|
|
1076
|
+
await queryRunner.query(
|
|
1077
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_pending_expires"`
|
|
1078
|
+
);
|
|
1079
|
+
await queryRunner.query(
|
|
1080
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_amount_status"`
|
|
1081
|
+
);
|
|
1082
|
+
await queryRunner.query(
|
|
1083
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_status_expires"`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
// src/migrations/1747700000000-FixIdempotencyAddTokenAddress.ts
|
|
1089
|
+
var FixIdempotencyAddTokenAddress1747700000000 = class {
|
|
1090
|
+
name = "FixIdempotencyAddTokenAddress1747700000000";
|
|
1091
|
+
async up(queryRunner) {
|
|
1092
|
+
await queryRunner.query(
|
|
1093
|
+
`DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
|
|
1094
|
+
);
|
|
1095
|
+
await queryRunner.query(`
|
|
1096
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_token_tx_reason"
|
|
1097
|
+
ON "ledger_journal" ("user_address", "token_address", "tx_hash", "reason")
|
|
1098
|
+
WHERE "tx_hash" IS NOT NULL
|
|
1099
|
+
`);
|
|
1100
|
+
}
|
|
1101
|
+
async down(queryRunner) {
|
|
1102
|
+
await queryRunner.query(
|
|
1103
|
+
`DROP INDEX IF EXISTS "UQ_ledger_journal_user_token_tx_reason"`
|
|
1104
|
+
);
|
|
1105
|
+
await queryRunner.query(`
|
|
1106
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
|
|
1107
|
+
ON "ledger_journal" ("user_address", "tx_hash", "reason")
|
|
1108
|
+
WHERE "tx_hash" IS NOT NULL
|
|
1109
|
+
`);
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
749
1113
|
// src/migrations/index.ts
|
|
750
|
-
var PAFI_MIGRATIONS = [
|
|
1114
|
+
var PAFI_MIGRATIONS = [
|
|
1115
|
+
InitialSchema1700000000000,
|
|
1116
|
+
CreateRedemptionHistory1746230400001,
|
|
1117
|
+
AddJournalIdempotencyIndex1747500000000,
|
|
1118
|
+
AddLockedMintCompositeIndexes1747600000000,
|
|
1119
|
+
FixIdempotencyAddTokenAddress1747700000000
|
|
1120
|
+
];
|
|
751
1121
|
// Annotate the CommonJS export names for ESM import in node:
|
|
752
1122
|
0 && (module.exports = {
|
|
1123
|
+
CreateRedemptionHistory1746230400001,
|
|
753
1124
|
IndexerCursorEntity,
|
|
754
1125
|
InitialSchema1700000000000,
|
|
755
1126
|
LedgerJournalEntity,
|
|
@@ -759,6 +1130,8 @@ var PAFI_MIGRATIONS = [InitialSchema1700000000000];
|
|
|
759
1130
|
PendingCreditEntity,
|
|
760
1131
|
PostgresCursorStore,
|
|
761
1132
|
PostgresPointLedger,
|
|
1133
|
+
PostgresRedemptionHistoryStore,
|
|
1134
|
+
RedemptionHistoryEntity,
|
|
762
1135
|
UserBalanceEntity
|
|
763
1136
|
});
|
|
764
1137
|
//# sourceMappingURL=index.cjs.map
|