@pafi-dev/issuer-postgres 0.1.2 → 0.2.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 +144 -0
- package/dist/entities/index.cjs +58 -1
- package/dist/entities/index.cjs.map +1 -1
- package/dist/entities/index.d.cts +33 -2
- package/dist/entities/index.d.ts +33 -2
- package/dist/entities/index.js +63 -1
- package/dist/entities/index.js.map +1 -1
- package/dist/index.cjs +215 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -4
- package/dist/index.d.ts +53 -4
- package/dist/index.js +218 -14
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +35 -1
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +17 -2
- package/dist/migrations/index.d.ts +17 -2
- package/dist/migrations/index.js +34 -1
- package/dist/migrations/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DataSource } from 'typeorm';
|
|
2
2
|
import { Address, Hex } from 'viem';
|
|
3
|
-
import { IPointLedger, LockedMintRequest, PendingCredit, MintingStatus, IIndexerCursorStore } from '@pafi-dev/issuer';
|
|
3
|
+
import { IPointLedger, LockedMintRequest, PendingCredit, MintingStatus, IIndexerCursorStore, IRedemptionHistoryStore } from '@pafi-dev/issuer';
|
|
4
4
|
import { LockedMintEntity, PendingCreditEntity } from './entities/index.cjs';
|
|
5
|
-
export { IndexerCursorEntity, LedgerJournalEntity, PAFI_ENTITIES, PendingCreditStatus, UserBalanceEntity } from './entities/index.cjs';
|
|
6
|
-
export { InitialSchema1700000000000, PAFI_MIGRATIONS } from './migrations/index.cjs';
|
|
5
|
+
export { IndexerCursorEntity, LedgerJournalEntity, PAFI_ENTITIES, PendingCreditStatus, RedemptionHistoryEntity, UserBalanceEntity } from './entities/index.cjs';
|
|
6
|
+
export { CreateRedemptionHistory1746230400001, InitialSchema1700000000000, PAFI_MIGRATIONS } from './migrations/index.cjs';
|
|
7
7
|
|
|
8
8
|
interface PostgresPointLedgerOptions {
|
|
9
9
|
/**
|
|
@@ -38,6 +38,32 @@ declare class PostgresPointLedger implements IPointLedger {
|
|
|
38
38
|
private readonly logger;
|
|
39
39
|
constructor(dataSource: DataSource, options?: PostgresPointLedgerOptions);
|
|
40
40
|
getBalance(userAddress: Address, tokenAddress?: Address): Promise<bigint>;
|
|
41
|
+
/**
|
|
42
|
+
* Background sweep — marks all expired PENDING locks as EXPIRED in
|
|
43
|
+
* a single UPDATE. Issuers SHOULD call this periodically (e.g.
|
|
44
|
+
* every 1-5 minutes via a cron / NestJS `@Interval`) to keep the
|
|
45
|
+
* lock table from growing unbounded.
|
|
46
|
+
*
|
|
47
|
+
* A single sweep amortizes the write cost vs `getBalance` doing a
|
|
48
|
+
* wide UPDATE on every read. Returns the number of rows transitioned.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { Interval } from "@nestjs/schedule";
|
|
53
|
+
*
|
|
54
|
+
* @Injectable()
|
|
55
|
+
* export class LockSweepService {
|
|
56
|
+
* constructor(private readonly ledger: PostgresPointLedger) {}
|
|
57
|
+
*
|
|
58
|
+
* @Interval(5 * 60 * 1000) // 5 minutes
|
|
59
|
+
* async sweep() {
|
|
60
|
+
* const swept = await this.ledger.markExpiredLocks();
|
|
61
|
+
* this.logger.debug(`expired ${swept} mint locks`);
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
markExpiredLocks(): Promise<number>;
|
|
41
67
|
getLockedRequests(userAddress: Address, tokenAddress?: Address): Promise<LockedMintRequest[]>;
|
|
42
68
|
getMintLock(lockId: string, userAddress?: Address): Promise<LockedMintRequest | null>;
|
|
43
69
|
/** Raw TypeORM row — escape hatch for callers that need entity fields. */
|
|
@@ -94,4 +120,27 @@ declare class PostgresCursorStore implements IIndexerCursorStore {
|
|
|
94
120
|
forKey(cursorId: string): IIndexerCursorStore;
|
|
95
121
|
}
|
|
96
122
|
|
|
97
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Postgres-backed IRedemptionHistoryStore. Append-only — every
|
|
125
|
+
* `recordRedemption` writes a new row. Reads (`sumRedeemedSince` /
|
|
126
|
+
* `getLastRedeemedAtUnixSec`) hit the (user, time) composite index.
|
|
127
|
+
*
|
|
128
|
+
* Addresses are normalized to checksum form on write and lower-cased
|
|
129
|
+
* on read so the index works regardless of casing inconsistency. The
|
|
130
|
+
* entity stores the canonical (checksummed) form.
|
|
131
|
+
*/
|
|
132
|
+
declare class PostgresRedemptionHistoryStore implements IRedemptionHistoryStore {
|
|
133
|
+
private readonly dataSource;
|
|
134
|
+
constructor(dataSource: DataSource);
|
|
135
|
+
sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
|
|
136
|
+
getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
|
|
137
|
+
recordRedemption(entry: {
|
|
138
|
+
user: Address;
|
|
139
|
+
amountPt: bigint;
|
|
140
|
+
pointTokenAddress?: Address;
|
|
141
|
+
unixSec: number;
|
|
142
|
+
reservationId?: string;
|
|
143
|
+
}): Promise<void>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { LockedMintEntity, PendingCreditEntity, PostgresCursorStore, PostgresPointLedger, type PostgresPointLedgerOptions, PostgresRedemptionHistoryStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DataSource } from 'typeorm';
|
|
2
2
|
import { Address, Hex } from 'viem';
|
|
3
|
-
import { IPointLedger, LockedMintRequest, PendingCredit, MintingStatus, IIndexerCursorStore } from '@pafi-dev/issuer';
|
|
3
|
+
import { IPointLedger, LockedMintRequest, PendingCredit, MintingStatus, IIndexerCursorStore, IRedemptionHistoryStore } from '@pafi-dev/issuer';
|
|
4
4
|
import { LockedMintEntity, PendingCreditEntity } from './entities/index.js';
|
|
5
|
-
export { IndexerCursorEntity, LedgerJournalEntity, PAFI_ENTITIES, PendingCreditStatus, UserBalanceEntity } from './entities/index.js';
|
|
6
|
-
export { InitialSchema1700000000000, PAFI_MIGRATIONS } from './migrations/index.js';
|
|
5
|
+
export { IndexerCursorEntity, LedgerJournalEntity, PAFI_ENTITIES, PendingCreditStatus, RedemptionHistoryEntity, UserBalanceEntity } from './entities/index.js';
|
|
6
|
+
export { CreateRedemptionHistory1746230400001, InitialSchema1700000000000, PAFI_MIGRATIONS } from './migrations/index.js';
|
|
7
7
|
|
|
8
8
|
interface PostgresPointLedgerOptions {
|
|
9
9
|
/**
|
|
@@ -38,6 +38,32 @@ declare class PostgresPointLedger implements IPointLedger {
|
|
|
38
38
|
private readonly logger;
|
|
39
39
|
constructor(dataSource: DataSource, options?: PostgresPointLedgerOptions);
|
|
40
40
|
getBalance(userAddress: Address, tokenAddress?: Address): Promise<bigint>;
|
|
41
|
+
/**
|
|
42
|
+
* Background sweep — marks all expired PENDING locks as EXPIRED in
|
|
43
|
+
* a single UPDATE. Issuers SHOULD call this periodically (e.g.
|
|
44
|
+
* every 1-5 minutes via a cron / NestJS `@Interval`) to keep the
|
|
45
|
+
* lock table from growing unbounded.
|
|
46
|
+
*
|
|
47
|
+
* A single sweep amortizes the write cost vs `getBalance` doing a
|
|
48
|
+
* wide UPDATE on every read. Returns the number of rows transitioned.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { Interval } from "@nestjs/schedule";
|
|
53
|
+
*
|
|
54
|
+
* @Injectable()
|
|
55
|
+
* export class LockSweepService {
|
|
56
|
+
* constructor(private readonly ledger: PostgresPointLedger) {}
|
|
57
|
+
*
|
|
58
|
+
* @Interval(5 * 60 * 1000) // 5 minutes
|
|
59
|
+
* async sweep() {
|
|
60
|
+
* const swept = await this.ledger.markExpiredLocks();
|
|
61
|
+
* this.logger.debug(`expired ${swept} mint locks`);
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
markExpiredLocks(): Promise<number>;
|
|
41
67
|
getLockedRequests(userAddress: Address, tokenAddress?: Address): Promise<LockedMintRequest[]>;
|
|
42
68
|
getMintLock(lockId: string, userAddress?: Address): Promise<LockedMintRequest | null>;
|
|
43
69
|
/** Raw TypeORM row — escape hatch for callers that need entity fields. */
|
|
@@ -94,4 +120,27 @@ declare class PostgresCursorStore implements IIndexerCursorStore {
|
|
|
94
120
|
forKey(cursorId: string): IIndexerCursorStore;
|
|
95
121
|
}
|
|
96
122
|
|
|
97
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Postgres-backed IRedemptionHistoryStore. Append-only — every
|
|
125
|
+
* `recordRedemption` writes a new row. Reads (`sumRedeemedSince` /
|
|
126
|
+
* `getLastRedeemedAtUnixSec`) hit the (user, time) composite index.
|
|
127
|
+
*
|
|
128
|
+
* Addresses are normalized to checksum form on write and lower-cased
|
|
129
|
+
* on read so the index works regardless of casing inconsistency. The
|
|
130
|
+
* entity stores the canonical (checksummed) form.
|
|
131
|
+
*/
|
|
132
|
+
declare class PostgresRedemptionHistoryStore implements IRedemptionHistoryStore {
|
|
133
|
+
private readonly dataSource;
|
|
134
|
+
constructor(dataSource: DataSource);
|
|
135
|
+
sumRedeemedSince(user: Address, sinceUnixSec: number, pointTokenAddress?: Address): Promise<bigint>;
|
|
136
|
+
getLastRedeemedAtUnixSec(user: Address, pointTokenAddress?: Address): Promise<number | null>;
|
|
137
|
+
recordRedemption(entry: {
|
|
138
|
+
user: Address;
|
|
139
|
+
amountPt: bigint;
|
|
140
|
+
pointTokenAddress?: Address;
|
|
141
|
+
unixSec: number;
|
|
142
|
+
reservationId?: string;
|
|
143
|
+
}): Promise<void>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { LockedMintEntity, PendingCreditEntity, PostgresCursorStore, PostgresPointLedger, type PostgresPointLedgerOptions, PostgresRedemptionHistoryStore };
|
package/dist/index.js
CHANGED
|
@@ -248,6 +248,34 @@ LedgerJournalEntity = __decorateClass([
|
|
|
248
248
|
], LedgerJournalEntity);
|
|
249
249
|
|
|
250
250
|
// src/postgresPointLedger.ts
|
|
251
|
+
var RETRIABLE_PG_CODES = /* @__PURE__ */ new Set(["40P01", "40001"]);
|
|
252
|
+
function isRetriablePgError(err) {
|
|
253
|
+
const e = err;
|
|
254
|
+
if (!e) return false;
|
|
255
|
+
if (e.code && RETRIABLE_PG_CODES.has(e.code)) return true;
|
|
256
|
+
if (e.message && /deadlock detected|could not serialize/i.test(e.message)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
async function withDeadlockRetry(dataSource, fn, maxAttempts = 3) {
|
|
262
|
+
let attempt = 0;
|
|
263
|
+
let delayMs = 25;
|
|
264
|
+
for (; ; ) {
|
|
265
|
+
attempt++;
|
|
266
|
+
try {
|
|
267
|
+
return await dataSource.transaction(fn);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (attempt >= maxAttempts || !isRetriablePgError(err)) {
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
await new Promise(
|
|
273
|
+
(r) => setTimeout(r, delayMs + Math.floor(Math.random() * delayMs))
|
|
274
|
+
);
|
|
275
|
+
delayMs *= 2;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
251
279
|
var PostgresPointLedger = class {
|
|
252
280
|
constructor(dataSource, options = {}) {
|
|
253
281
|
this.dataSource = dataSource;
|
|
@@ -260,13 +288,47 @@ var PostgresPointLedger = class {
|
|
|
260
288
|
// ---------------------------------------------------------------------
|
|
261
289
|
async getBalance(userAddress, tokenAddress) {
|
|
262
290
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
263
|
-
|
|
264
|
-
|
|
291
|
+
const [balanceRow, locked] = await Promise.all([
|
|
292
|
+
this.dataSource.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } }),
|
|
293
|
+
this.sumPendingLocks(user, token)
|
|
294
|
+
]);
|
|
265
295
|
const total = balanceRow?.balance ?? 0n;
|
|
266
|
-
const locked = await this.sumPendingLocks(user, token);
|
|
267
296
|
const available = total - locked;
|
|
268
297
|
return available < 0n ? 0n : available;
|
|
269
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Background sweep — marks all expired PENDING locks as EXPIRED in
|
|
301
|
+
* a single UPDATE. Issuers SHOULD call this periodically (e.g.
|
|
302
|
+
* every 1-5 minutes via a cron / NestJS `@Interval`) to keep the
|
|
303
|
+
* lock table from growing unbounded.
|
|
304
|
+
*
|
|
305
|
+
* A single sweep amortizes the write cost vs `getBalance` doing a
|
|
306
|
+
* wide UPDATE on every read. Returns the number of rows transitioned.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* import { Interval } from "@nestjs/schedule";
|
|
311
|
+
*
|
|
312
|
+
* @Injectable()
|
|
313
|
+
* export class LockSweepService {
|
|
314
|
+
* constructor(private readonly ledger: PostgresPointLedger) {}
|
|
315
|
+
*
|
|
316
|
+
* @Interval(5 * 60 * 1000) // 5 minutes
|
|
317
|
+
* async sweep() {
|
|
318
|
+
* const swept = await this.ledger.markExpiredLocks();
|
|
319
|
+
* this.logger.debug(`expired ${swept} mint locks`);
|
|
320
|
+
* }
|
|
321
|
+
* }
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
async markExpiredLocks() {
|
|
325
|
+
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();
|
|
326
|
+
const swept = result.affected ?? 0;
|
|
327
|
+
if (swept > 0) {
|
|
328
|
+
this.logger?.debug?.(`markExpiredLocks: swept ${swept} mint locks`);
|
|
329
|
+
}
|
|
330
|
+
return swept;
|
|
331
|
+
}
|
|
270
332
|
async getLockedRequests(userAddress, tokenAddress) {
|
|
271
333
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
272
334
|
const rows = await this.dataSource.getRepository(LockedMintEntity).find({
|
|
@@ -332,8 +394,8 @@ var PostgresPointLedger = class {
|
|
|
332
394
|
throw new Error("creditBalance: amount must be positive");
|
|
333
395
|
}
|
|
334
396
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
335
|
-
await this.dataSource
|
|
336
|
-
const existing = await tx.getRepository(UserBalanceEntity).
|
|
397
|
+
await withDeadlockRetry(this.dataSource, async (tx) => {
|
|
398
|
+
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
399
|
const next = (existing?.balance ?? 0n) + amount;
|
|
338
400
|
await tx.getRepository(UserBalanceEntity).upsert(
|
|
339
401
|
{ userAddress: user, tokenAddress: token, balance: next },
|
|
@@ -356,8 +418,8 @@ var PostgresPointLedger = class {
|
|
|
356
418
|
throw new Error("lockForMinting: lockDurationMs must be positive");
|
|
357
419
|
}
|
|
358
420
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
359
|
-
return this.dataSource
|
|
360
|
-
const balanceRow = await tx.getRepository(UserBalanceEntity).
|
|
421
|
+
return withDeadlockRetry(this.dataSource, async (tx) => {
|
|
422
|
+
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
423
|
const total = balanceRow?.balance ?? 0n;
|
|
362
424
|
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
425
|
const locked = pendingTotal ? BigInt(pendingTotal.sum) : 0n;
|
|
@@ -391,8 +453,8 @@ var PostgresPointLedger = class {
|
|
|
391
453
|
throw new Error("deductBalance: amount must be positive");
|
|
392
454
|
}
|
|
393
455
|
const { user, token } = normalize(userAddress, tokenAddress);
|
|
394
|
-
await this.dataSource
|
|
395
|
-
const balance = await tx.getRepository(UserBalanceEntity).
|
|
456
|
+
await withDeadlockRetry(this.dataSource, async (tx) => {
|
|
457
|
+
const balance = await tx.getRepository(UserBalanceEntity).createQueryBuilder("balance").setLock("pessimistic_write").where("balance.user_address = :user", { user }).andWhere("balance.token_address = :token", { token }).getOne();
|
|
396
458
|
if (!balance || balance.balance < amount) {
|
|
397
459
|
throw new Error(
|
|
398
460
|
`Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`
|
|
@@ -459,8 +521,8 @@ var PostgresPointLedger = class {
|
|
|
459
521
|
return row.id;
|
|
460
522
|
}
|
|
461
523
|
async resolveCreditByBurnTx(lockId, txHash) {
|
|
462
|
-
await this.dataSource
|
|
463
|
-
const credit = await tx.getRepository(PendingCreditEntity).
|
|
524
|
+
await withDeadlockRetry(this.dataSource, async (tx) => {
|
|
525
|
+
const credit = await tx.getRepository(PendingCreditEntity).createQueryBuilder("credit").setLock("pessimistic_write").where("credit.id = :id", { id: lockId }).getOne();
|
|
464
526
|
if (!credit) {
|
|
465
527
|
throw new Error(
|
|
466
528
|
`resolveCreditByBurnTx: unknown pending credit ${lockId}`
|
|
@@ -494,7 +556,7 @@ var PostgresPointLedger = class {
|
|
|
494
556
|
);
|
|
495
557
|
return;
|
|
496
558
|
}
|
|
497
|
-
const balance = await tx.getRepository(UserBalanceEntity).
|
|
559
|
+
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
560
|
const next = (balance?.balance ?? 0n) + credit.amount;
|
|
499
561
|
await tx.getRepository(UserBalanceEntity).upsert(
|
|
500
562
|
{ userAddress: user, tokenAddress: token, balance: next },
|
|
@@ -620,13 +682,120 @@ var PostgresCursorStore = class _PostgresCursorStore {
|
|
|
620
682
|
}
|
|
621
683
|
};
|
|
622
684
|
|
|
685
|
+
// src/postgresRedemptionHistoryStore.ts
|
|
686
|
+
import { getAddress as getAddress2 } from "viem";
|
|
687
|
+
|
|
688
|
+
// src/entities/redemption-history.entity.ts
|
|
689
|
+
import {
|
|
690
|
+
Column as Column6,
|
|
691
|
+
CreateDateColumn as CreateDateColumn4,
|
|
692
|
+
Entity as Entity6,
|
|
693
|
+
Index as Index4,
|
|
694
|
+
PrimaryGeneratedColumn as PrimaryGeneratedColumn4
|
|
695
|
+
} from "typeorm";
|
|
696
|
+
var RedemptionHistoryEntity = class {
|
|
697
|
+
id;
|
|
698
|
+
userAddress;
|
|
699
|
+
tokenAddress;
|
|
700
|
+
amountPt;
|
|
701
|
+
createdAtUnixSec;
|
|
702
|
+
reservationId;
|
|
703
|
+
rowCreatedAt;
|
|
704
|
+
};
|
|
705
|
+
__decorateClass([
|
|
706
|
+
PrimaryGeneratedColumn4("uuid")
|
|
707
|
+
], RedemptionHistoryEntity.prototype, "id", 2);
|
|
708
|
+
__decorateClass([
|
|
709
|
+
Column6({ name: "user_address", type: "varchar", length: 42 })
|
|
710
|
+
], RedemptionHistoryEntity.prototype, "userAddress", 2);
|
|
711
|
+
__decorateClass([
|
|
712
|
+
Column6({ name: "token_address", type: "varchar", length: 42, nullable: true })
|
|
713
|
+
], RedemptionHistoryEntity.prototype, "tokenAddress", 2);
|
|
714
|
+
__decorateClass([
|
|
715
|
+
Column6({
|
|
716
|
+
name: "amount_pt",
|
|
717
|
+
type: "numeric",
|
|
718
|
+
precision: 78,
|
|
719
|
+
scale: 0,
|
|
720
|
+
transformer: {
|
|
721
|
+
to: (value) => value.toString(),
|
|
722
|
+
from: (value) => BigInt(value)
|
|
723
|
+
}
|
|
724
|
+
})
|
|
725
|
+
], RedemptionHistoryEntity.prototype, "amountPt", 2);
|
|
726
|
+
__decorateClass([
|
|
727
|
+
Column6({ name: "created_at_unix_sec", type: "bigint" })
|
|
728
|
+
], RedemptionHistoryEntity.prototype, "createdAtUnixSec", 2);
|
|
729
|
+
__decorateClass([
|
|
730
|
+
Column6({
|
|
731
|
+
name: "reservation_id",
|
|
732
|
+
type: "varchar",
|
|
733
|
+
length: 64,
|
|
734
|
+
nullable: true
|
|
735
|
+
})
|
|
736
|
+
], RedemptionHistoryEntity.prototype, "reservationId", 2);
|
|
737
|
+
__decorateClass([
|
|
738
|
+
CreateDateColumn4({ name: "row_created_at", type: "timestamptz" })
|
|
739
|
+
], RedemptionHistoryEntity.prototype, "rowCreatedAt", 2);
|
|
740
|
+
RedemptionHistoryEntity = __decorateClass([
|
|
741
|
+
Entity6({ name: "redemption_history" }),
|
|
742
|
+
Index4("idx_redemption_history_user_created", [
|
|
743
|
+
"userAddress",
|
|
744
|
+
"createdAtUnixSec"
|
|
745
|
+
])
|
|
746
|
+
], RedemptionHistoryEntity);
|
|
747
|
+
|
|
748
|
+
// src/postgresRedemptionHistoryStore.ts
|
|
749
|
+
var PostgresRedemptionHistoryStore = class {
|
|
750
|
+
constructor(dataSource) {
|
|
751
|
+
this.dataSource = dataSource;
|
|
752
|
+
}
|
|
753
|
+
dataSource;
|
|
754
|
+
async sumRedeemedSince(user, sinceUnixSec, pointTokenAddress) {
|
|
755
|
+
const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
|
|
756
|
+
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 });
|
|
757
|
+
if (pointTokenAddress !== void 0) {
|
|
758
|
+
qb.andWhere("rh.token_address = :token", {
|
|
759
|
+
token: getAddress2(pointTokenAddress)
|
|
760
|
+
});
|
|
761
|
+
} else {
|
|
762
|
+
}
|
|
763
|
+
const row = await qb.getRawOne() ?? { sum: "0" };
|
|
764
|
+
return BigInt(row.sum ?? "0");
|
|
765
|
+
}
|
|
766
|
+
async getLastRedeemedAtUnixSec(user, pointTokenAddress) {
|
|
767
|
+
const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
|
|
768
|
+
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);
|
|
769
|
+
if (pointTokenAddress !== void 0) {
|
|
770
|
+
qb.andWhere("rh.token_address = :token", {
|
|
771
|
+
token: getAddress2(pointTokenAddress)
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const row = await qb.getRawOne();
|
|
775
|
+
if (!row || row.ts === null) return null;
|
|
776
|
+
return Number(row.ts);
|
|
777
|
+
}
|
|
778
|
+
async recordRedemption(entry) {
|
|
779
|
+
const repo = this.dataSource.getRepository(RedemptionHistoryEntity);
|
|
780
|
+
const row = repo.create({
|
|
781
|
+
userAddress: getAddress2(entry.user),
|
|
782
|
+
tokenAddress: entry.pointTokenAddress ? getAddress2(entry.pointTokenAddress) : null,
|
|
783
|
+
amountPt: entry.amountPt,
|
|
784
|
+
createdAtUnixSec: String(entry.unixSec),
|
|
785
|
+
reservationId: entry.reservationId ?? null
|
|
786
|
+
});
|
|
787
|
+
await repo.save(row);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
623
791
|
// src/entities/index.ts
|
|
624
792
|
var PAFI_ENTITIES = [
|
|
625
793
|
LockedMintEntity,
|
|
626
794
|
PendingCreditEntity,
|
|
627
795
|
UserBalanceEntity,
|
|
628
796
|
LedgerJournalEntity,
|
|
629
|
-
IndexerCursorEntity
|
|
797
|
+
IndexerCursorEntity,
|
|
798
|
+
RedemptionHistoryEntity
|
|
630
799
|
];
|
|
631
800
|
|
|
632
801
|
// src/migrations/1700000000000-InitialSchema.ts
|
|
@@ -732,9 +901,42 @@ var InitialSchema1700000000000 = class {
|
|
|
732
901
|
}
|
|
733
902
|
};
|
|
734
903
|
|
|
904
|
+
// src/migrations/1746230400001-CreateRedemptionHistory.ts
|
|
905
|
+
var CreateRedemptionHistory1746230400001 = class {
|
|
906
|
+
name = "CreateRedemptionHistory1746230400001";
|
|
907
|
+
async up(queryRunner) {
|
|
908
|
+
await queryRunner.query(`
|
|
909
|
+
CREATE TABLE IF NOT EXISTS redemption_history (
|
|
910
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
911
|
+
user_address varchar(42) NOT NULL,
|
|
912
|
+
token_address varchar(42),
|
|
913
|
+
amount_pt numeric(78, 0) NOT NULL,
|
|
914
|
+
created_at_unix_sec bigint NOT NULL,
|
|
915
|
+
reservation_id varchar(64),
|
|
916
|
+
row_created_at timestamptz NOT NULL DEFAULT NOW(),
|
|
917
|
+
CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)
|
|
918
|
+
)
|
|
919
|
+
`);
|
|
920
|
+
await queryRunner.query(`
|
|
921
|
+
CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created
|
|
922
|
+
ON redemption_history (user_address, created_at_unix_sec DESC)
|
|
923
|
+
`);
|
|
924
|
+
}
|
|
925
|
+
async down(queryRunner) {
|
|
926
|
+
await queryRunner.query(
|
|
927
|
+
`DROP INDEX IF EXISTS idx_redemption_history_user_created`
|
|
928
|
+
);
|
|
929
|
+
await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
|
|
735
933
|
// src/migrations/index.ts
|
|
736
|
-
var PAFI_MIGRATIONS = [
|
|
934
|
+
var PAFI_MIGRATIONS = [
|
|
935
|
+
InitialSchema1700000000000,
|
|
936
|
+
CreateRedemptionHistory1746230400001
|
|
937
|
+
];
|
|
737
938
|
export {
|
|
939
|
+
CreateRedemptionHistory1746230400001,
|
|
738
940
|
IndexerCursorEntity,
|
|
739
941
|
InitialSchema1700000000000,
|
|
740
942
|
LedgerJournalEntity,
|
|
@@ -744,6 +946,8 @@ export {
|
|
|
744
946
|
PendingCreditEntity,
|
|
745
947
|
PostgresCursorStore,
|
|
746
948
|
PostgresPointLedger,
|
|
949
|
+
PostgresRedemptionHistoryStore,
|
|
950
|
+
RedemptionHistoryEntity,
|
|
747
951
|
UserBalanceEntity
|
|
748
952
|
};
|
|
749
953
|
//# sourceMappingURL=index.js.map
|