@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 CHANGED
@@ -10,10 +10,9 @@ schema from the reference issuer.
10
10
 
11
11
  > **Server-only.** Pulls in TypeORM (peer-dep). Do not bundle into a browser app.
12
12
 
13
- > **Compatible with `@pafi-dev/issuer@0.6.x`.** Schema baseline includes
13
+ > **Compatible with `@pafi-dev/issuer`.** Schema baseline includes
14
14
  > `user_op_hash` columns + indexes for the mobile prepare/submit
15
- > bundler-receipt fallback. No migration needed when bumping issuer
16
- > SDK within the 0.6 line.
15
+ > bundler-receipt fallback.
17
16
 
18
17
  ---
19
18
 
@@ -70,6 +69,11 @@ import {
70
69
  entities: [...PAFI_ENTITIES /*, ...yourCustomEntities */],
71
70
  migrations: [...PAFI_MIGRATIONS /*, ...yourCustomMigrations */],
72
71
  migrationsRun: true,
72
+ // Required — some shipped migrations use `CREATE INDEX
73
+ // CONCURRENTLY` which cannot run inside a transaction.
74
+ // The default "all" mode wraps every migration in one tx
75
+ // and rejects per-migration `transaction = false` opt-outs.
76
+ migrationsTransactionMode: "each",
73
77
  }),
74
78
  ],
75
79
  providers: [
@@ -178,6 +182,173 @@ extending without breaking the schema is straightforward.
178
182
 
179
183
  ---
180
184
 
185
+ ## Production deployment checklist
186
+
187
+ Recommended Postgres + TypeORM config for an issuer running real-money
188
+ balances.
189
+
190
+ ### 1. Connection pool sizing
191
+
192
+ TypeORM defaults inherit from `pg`'s defaults (`max: 10`, no idle
193
+ timeout) which silently throttles under load. Tune via
194
+ `DataSourceOptions.extra`:
195
+
196
+ ```ts
197
+ const dataSource = new DataSource({
198
+ type: "postgres",
199
+ url: process.env.DATABASE_URL,
200
+ entities: [...PAFI_ENTITIES, ...yourEntities],
201
+ migrations: [...PAFI_MIGRATIONS],
202
+ extra: {
203
+ // Per-pod pool max — total = max × pod count. Stay under
204
+ // Postgres's `max_connections` (default 100; raise via
205
+ // `ALTER SYSTEM SET max_connections = 200` if needed).
206
+ max: 20,
207
+ // Drop idle connections after 30s — frees slots for other pods
208
+ // and avoids hitting per-connection memory bloat.
209
+ idleTimeoutMillis: 30_000,
210
+ // Connection acquisition deadline — fail fast instead of
211
+ // queueing requests indefinitely under back-pressure.
212
+ connectionTimeoutMillis: 5_000,
213
+ // Per-statement deadline (also see "statement_timeout" below).
214
+ // 30s is a generous cap for read queries; tighten to 5s in
215
+ // production once you've measured tail latencies.
216
+ statement_timeout: 30_000,
217
+ },
218
+ });
219
+ ```
220
+
221
+ ### 2. Server-side timeouts
222
+
223
+ Set at the database role level (run once during issuer onboarding):
224
+
225
+ ```sql
226
+ -- Per-statement deadline — backstop against runaway queries holding
227
+ -- a connection indefinitely. 30s is conservative; tune lower once
228
+ -- you've measured your p99.
229
+ ALTER ROLE issuer_app SET statement_timeout = '30s';
230
+
231
+ -- Auto-rollback transactions that idle without sending another query.
232
+ -- Critical pairing with FOR UPDATE: a stale transaction holds row
233
+ -- locks, blocking concurrent claim/redeem. 60s is safe; aggressive
234
+ -- shops set 10s.
235
+ ALTER ROLE issuer_app SET idle_in_transaction_session_timeout = '60s';
236
+
237
+ -- Lock acquisition deadline — fail fast on deadlock-adjacent waits
238
+ -- instead of waiting for Postgres's default deadlock_timeout (1s)
239
+ -- followed by automatic retry.
240
+ ALTER ROLE issuer_app SET lock_timeout = '10s';
241
+ ```
242
+
243
+ ### 3. Expired lock sweep
244
+
245
+ `getBalance` is a pure read (does not transition expired locks
246
+ itself). Schedule `markExpiredLocks()` periodically to keep the
247
+ `locked_mint_requests` table from growing unbounded:
248
+
249
+ ```ts
250
+ import { Interval } from "@nestjs/schedule";
251
+
252
+ @Injectable()
253
+ export class LockSweepService {
254
+ private readonly logger = new Logger(LockSweepService.name);
255
+
256
+ constructor(@Inject("PAFI_LEDGER") private readonly ledger: PostgresPointLedger) {}
257
+
258
+ // Every 5 minutes — cheap UPDATE, single round trip.
259
+ @Interval(5 * 60 * 1000)
260
+ async sweep() {
261
+ try {
262
+ const swept = await this.ledger.markExpiredLocks();
263
+ if (swept > 0) this.logger.debug(`expired ${swept} mint locks`);
264
+ } catch (err) {
265
+ this.logger.error("lock sweep failed", err);
266
+ }
267
+ }
268
+ }
269
+ ```
270
+
271
+ Also wire `markExpiredCredits()` (PendingCredit equivalent) the same way.
272
+
273
+ ### 4. Required indexes
274
+
275
+ Shipped migrations (`InitialSchema` + `AddLockedMintCompositeIndexes`)
276
+ create these indexes on `locked_mint_requests`. Verify after migration:
277
+
278
+ ```sql
279
+ SELECT indexname, indexdef
280
+ FROM pg_indexes
281
+ WHERE tablename = 'locked_mint_requests'
282
+ ORDER BY indexname;
283
+ ```
284
+
285
+ Expected output:
286
+
287
+ ```sql
288
+ -- Hot path: sumPendingLocks (during getBalance + lockForMinting)
289
+ CREATE INDEX "IDX_locked_mint_user_token_status_expires"
290
+ ON locked_mint_requests (user_address, token_address, status, expires_at);
291
+
292
+ -- Hot path: PointIndexer.pickMatchingLock + deductBalance findOne
293
+ CREATE INDEX "IDX_locked_mint_user_token_amount_status"
294
+ ON locked_mint_requests (user_address, token_address, amount, status);
295
+
296
+ -- Sweep path: markExpiredLocks UPDATE (partial — only PENDING rows)
297
+ CREATE INDEX "IDX_locked_mint_pending_expires"
298
+ ON locked_mint_requests (expires_at) WHERE status = 'PENDING';
299
+
300
+ -- Bundler-receipt fallback in statusHandlers
301
+ CREATE INDEX "IDX_locked_mint_user_op_hash"
302
+ ON locked_mint_requests (user_op_hash);
303
+ ```
304
+
305
+ Plus the partial unique index on `ledger_journal` that enforces
306
+ indexer idempotency (`MINT_CONFIRMED`, `BURN_FOR_CREDIT`):
307
+
308
+ ```sql
309
+ CREATE UNIQUE INDEX "UQ_ledger_journal_user_tx_reason"
310
+ ON ledger_journal (user_address, tx_hash, reason)
311
+ WHERE tx_hash IS NOT NULL;
312
+ ```
313
+
314
+ ### 5. Monitoring queries
315
+
316
+ Surface key health metrics to your APM (Datadog / Grafana / Sentry):
317
+
318
+ ```sql
319
+ -- Outstanding PENDING locks per token (alert if > N)
320
+ SELECT token_address, COUNT(*) AS pending_count, SUM(amount::numeric) AS total_locked
321
+ FROM locked_mint_requests
322
+ WHERE status = 'PENDING' AND expires_at > NOW()
323
+ GROUP BY token_address;
324
+
325
+ -- Pool saturation
326
+ SELECT count(*), state FROM pg_stat_activity
327
+ WHERE datname = current_database()
328
+ GROUP BY state;
329
+
330
+ -- Long-running locks (potential deadlock indicator)
331
+ SELECT pid, now() - xact_start AS duration, state, query
332
+ FROM pg_stat_activity
333
+ WHERE state IN ('active','idle in transaction')
334
+ AND now() - xact_start > interval '10 seconds'
335
+ ORDER BY duration DESC;
336
+ ```
337
+
338
+ ### 6. Backup + recovery
339
+
340
+ Ledger is the source of truth for off-chain points balance. Configure:
341
+
342
+ - Continuous WAL archiving (`pg_basebackup` + `archive_command`)
343
+ - Daily logical dumps (`pg_dump --format=custom`) retained 30+ days
344
+ - Point-in-time recovery validated quarterly via dry-run restore
345
+
346
+ Loss of `user_balances` rows = customer rebate liability proportional
347
+ to historic mints. Treat backup verification as a security control,
348
+ not an operational nice-to-have.
349
+
350
+ ---
351
+
181
352
  ## License
182
353
 
183
354
  Apache-2.0
@@ -33,6 +33,7 @@ __export(entities_exports, {
33
33
  LockedMintEntity: () => LockedMintEntity,
34
34
  PAFI_ENTITIES: () => PAFI_ENTITIES,
35
35
  PendingCreditEntity: () => PendingCreditEntity,
36
+ RedemptionHistoryEntity: () => RedemptionHistoryEntity,
36
37
  UserBalanceEntity: () => UserBalanceEntity
37
38
  });
38
39
  module.exports = __toCommonJS(entities_exports);
@@ -98,7 +99,22 @@ __decorateClass([
98
99
  ], LockedMintEntity.prototype, "userOpHash", 2);
99
100
  LockedMintEntity = __decorateClass([
100
101
  (0, import_typeorm.Entity)({ name: "locked_mint_requests" }),
101
- (0, import_typeorm.Index)(["userAddress", "status"])
102
+ (0, import_typeorm.Index)("IDX_locked_mint_user_token_status_expires", [
103
+ "userAddress",
104
+ "tokenAddress",
105
+ "status",
106
+ "expiresAt"
107
+ ]),
108
+ (0, import_typeorm.Index)("IDX_locked_mint_user_token_amount_status", [
109
+ "userAddress",
110
+ "tokenAddress",
111
+ "amount",
112
+ "status"
113
+ ]),
114
+ (0, import_typeorm.Index)("IDX_locked_mint_pending_expires", ["expiresAt"], {
115
+ where: `"status" = 'PENDING'`
116
+ }),
117
+ (0, import_typeorm.Index)("IDX_locked_mint_user_op_hash", ["userOpHash"])
102
118
  ], LockedMintEntity);
103
119
 
104
120
  // src/entities/pending-credit.entity.ts
@@ -251,7 +267,15 @@ __decorateClass([
251
267
  ], LedgerJournalEntity.prototype, "createdAt", 2);
252
268
  LedgerJournalEntity = __decorateClass([
253
269
  (0, import_typeorm4.Entity)({ name: "ledger_journal" }),
254
- (0, import_typeorm4.Index)(["userAddress", "createdAt"])
270
+ (0, import_typeorm4.Index)(["userAddress", "createdAt"]),
271
+ (0, import_typeorm4.Index)(
272
+ "UQ_ledger_journal_user_token_tx_reason",
273
+ ["userAddress", "tokenAddress", "txHash", "reason"],
274
+ {
275
+ unique: true,
276
+ where: '"tx_hash" IS NOT NULL'
277
+ }
278
+ )
255
279
  ], LedgerJournalEntity);
256
280
 
257
281
  // src/entities/indexer-cursor.entity.ts
@@ -283,13 +307,68 @@ IndexerCursorEntity = __decorateClass([
283
307
  (0, import_typeorm5.Entity)({ name: "indexer_cursors" })
284
308
  ], IndexerCursorEntity);
285
309
 
310
+ // src/entities/redemption-history.entity.ts
311
+ var import_typeorm6 = require("typeorm");
312
+ var RedemptionHistoryEntity = class {
313
+ id;
314
+ userAddress;
315
+ tokenAddress;
316
+ amountPt;
317
+ createdAtUnixSec;
318
+ reservationId;
319
+ rowCreatedAt;
320
+ };
321
+ __decorateClass([
322
+ (0, import_typeorm6.PrimaryGeneratedColumn)("uuid")
323
+ ], RedemptionHistoryEntity.prototype, "id", 2);
324
+ __decorateClass([
325
+ (0, import_typeorm6.Column)({ name: "user_address", type: "varchar", length: 42 })
326
+ ], RedemptionHistoryEntity.prototype, "userAddress", 2);
327
+ __decorateClass([
328
+ (0, import_typeorm6.Column)({ name: "token_address", type: "varchar", length: 42, nullable: true })
329
+ ], RedemptionHistoryEntity.prototype, "tokenAddress", 2);
330
+ __decorateClass([
331
+ (0, import_typeorm6.Column)({
332
+ name: "amount_pt",
333
+ type: "numeric",
334
+ precision: 78,
335
+ scale: 0,
336
+ transformer: {
337
+ to: (value) => value.toString(),
338
+ from: (value) => BigInt(value)
339
+ }
340
+ })
341
+ ], RedemptionHistoryEntity.prototype, "amountPt", 2);
342
+ __decorateClass([
343
+ (0, import_typeorm6.Column)({ name: "created_at_unix_sec", type: "bigint" })
344
+ ], RedemptionHistoryEntity.prototype, "createdAtUnixSec", 2);
345
+ __decorateClass([
346
+ (0, import_typeorm6.Column)({
347
+ name: "reservation_id",
348
+ type: "varchar",
349
+ length: 64,
350
+ nullable: true
351
+ })
352
+ ], RedemptionHistoryEntity.prototype, "reservationId", 2);
353
+ __decorateClass([
354
+ (0, import_typeorm6.CreateDateColumn)({ name: "row_created_at", type: "timestamptz" })
355
+ ], RedemptionHistoryEntity.prototype, "rowCreatedAt", 2);
356
+ RedemptionHistoryEntity = __decorateClass([
357
+ (0, import_typeorm6.Entity)({ name: "redemption_history" }),
358
+ (0, import_typeorm6.Index)("idx_redemption_history_user_created", [
359
+ "userAddress",
360
+ "createdAtUnixSec"
361
+ ])
362
+ ], RedemptionHistoryEntity);
363
+
286
364
  // src/entities/index.ts
287
365
  var PAFI_ENTITIES = [
288
366
  LockedMintEntity,
289
367
  PendingCreditEntity,
290
368
  UserBalanceEntity,
291
369
  LedgerJournalEntity,
292
- IndexerCursorEntity
370
+ IndexerCursorEntity,
371
+ RedemptionHistoryEntity
293
372
  ];
294
373
  // Annotate the CommonJS export names for ESM import in node:
295
374
  0 && (module.exports = {
@@ -298,6 +377,7 @@ var PAFI_ENTITIES = [
298
377
  LockedMintEntity,
299
378
  PAFI_ENTITIES,
300
379
  PendingCreditEntity,
380
+ RedemptionHistoryEntity,
301
381
  UserBalanceEntity
302
382
  });
303
383
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/entities/index.ts","../../src/entities/locked-mint.entity.ts","../../src/entities/pending-credit.entity.ts","../../src/entities/user-balance.entity.ts","../../src/entities/ledger-journal.entity.ts","../../src/entities/indexer-cursor.entity.ts"],"sourcesContent":["export { LockedMintEntity } from \"./locked-mint.entity\";\nexport {\n PendingCreditEntity,\n type PendingCreditStatus,\n} from \"./pending-credit.entity\";\nexport { UserBalanceEntity } from \"./user-balance.entity\";\nexport { LedgerJournalEntity } from \"./ledger-journal.entity\";\nexport { IndexerCursorEntity } from \"./indexer-cursor.entity\";\n\nimport { LockedMintEntity } from \"./locked-mint.entity\";\nimport { PendingCreditEntity } from \"./pending-credit.entity\";\nimport { UserBalanceEntity } from \"./user-balance.entity\";\nimport { LedgerJournalEntity } from \"./ledger-journal.entity\";\nimport { IndexerCursorEntity } from \"./indexer-cursor.entity\";\n\n/**\n * All entities in one array — drop into TypeORM's `entities` config or\n * NestJS's `TypeOrmModule.forFeature(PAFI_ENTITIES)`.\n */\nexport const PAFI_ENTITIES = [\n LockedMintEntity,\n PendingCreditEntity,\n UserBalanceEntity,\n LedgerJournalEntity,\n IndexerCursorEntity,\n] as const;\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\nimport type { MintingStatus } from \"@pafi-dev/issuer\";\n\n/**\n * A reservation against a user's off-chain balance.\n *\n * Lifecycle:\n * PENDING ── PointIndexer matches Mint event ──▶ MINTED\n * │\n * ├── deadline elapsed ────────────────────▶ EXPIRED\n * │\n * └── tx reverted ─────────────────────────▶ FAILED\n *\n * The `(userAddress, status)` composite index keeps the \"sum all\n * PENDING locks\" hot path of `lockForMinting()` fast under load.\n */\n@Entity({ name: \"locked_mint_requests\" })\n@Index([\"userAddress\", \"status\"])\nexport class LockedMintEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"amount\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n amount!: bigint;\n\n @Column({\n name: \"status\",\n type: \"varchar\",\n length: 16,\n default: \"PENDING\",\n })\n status!: MintingStatus;\n\n @CreateDateColumn({ name: \"created_at\" })\n createdAt!: Date;\n\n @Column({ name: \"expires_at\", type: \"timestamp with time zone\" })\n expiresAt!: Date;\n\n @Column({ name: \"tx_hash\", type: \"varchar\", length: 66, nullable: true })\n txHash?: string | null;\n\n /**\n * ERC-4337 userOpHash returned by the bundler at /claim/submit.\n * Bound to the lock so `/claim/status` can fall back to the bundler\n * receipt — required when multiple PENDING locks share the same\n * `amount` (PointIndexer matches by amount and can pick the wrong\n * sibling lock).\n */\n @Column({\n name: \"user_op_hash\",\n type: \"varchar\",\n length: 66,\n nullable: true,\n })\n userOpHash?: string | null;\n}\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\n\nexport type PendingCreditStatus = \"PENDING\" | \"RESOLVED\" | \"EXPIRED\";\n\n/**\n * Reverse flow — user burns on-chain PT, `BurnIndexer` observes\n * `Transfer(user → 0x0)`, the credit is settled to the off-chain ledger.\n *\n * Lifecycle:\n * PENDING ── Burn tx observed by indexer ──▶ RESOLVED\n * │\n * └── deadline elapsed ────────────────▶ EXPIRED\n *\n * The credit is reserved BEFORE the UserOp is submitted so the\n * indexer can correlate `(user, amount, token)` back to the off-chain\n * row when the burn lands.\n */\n@Entity({ name: \"pending_credits\" })\n@Index([\"userAddress\", \"status\"])\n@Index([\"txHash\"])\nexport class PendingCreditEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"amount\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n amount!: bigint;\n\n @Column({\n name: \"status\",\n type: \"varchar\",\n length: 16,\n default: \"PENDING\",\n })\n status!: PendingCreditStatus;\n\n /** On-chain burn tx that settled this credit. Null until indexer resolves. */\n @Column({ name: \"tx_hash\", type: \"varchar\", length: 66, nullable: true })\n txHash?: string | null;\n\n /** ERC-4337 userOpHash bound at /redeem/submit. See `LockedMintEntity`. */\n @Column({\n name: \"user_op_hash\",\n type: \"varchar\",\n length: 66,\n nullable: true,\n })\n userOpHash?: string | null;\n\n @CreateDateColumn({ name: \"created_at\" })\n createdAt!: Date;\n\n @Column({ name: \"expires_at\", type: \"timestamp with time zone\" })\n expiresAt!: Date;\n\n @Column({\n name: \"resolved_at\",\n type: \"timestamp with time zone\",\n nullable: true,\n })\n resolvedAt?: Date | null;\n}\n","import { Column, Entity, PrimaryColumn, UpdateDateColumn } from \"typeorm\";\n\n/**\n * Off-chain point balance per `(userAddress, tokenAddress)`.\n *\n * `balance` is the **total** owned; pending reservations live in\n * `LockedMintEntity`. Available balance = total − sum(PENDING locks)\n * — `getBalance` in `PostgresPointLedger` does this subtraction\n * inside a transaction so reads are race-free.\n *\n * All amounts are `numeric(78, 0)` for full bigint precision (uint256\n * fits in 78 decimal digits). TypeORM transforms bigint ↔ string at\n * the boundary; in JS/TS code we always deal with `bigint`.\n */\n@Entity({ name: \"user_balances\" })\nexport class UserBalanceEntity {\n @PrimaryColumn({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @PrimaryColumn({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"balance\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n default: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n balance!: bigint;\n\n @UpdateDateColumn({ name: \"updated_at\" })\n updatedAt!: Date;\n}\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\n\n/**\n * Append-only audit trail for every balance mutation. Used for\n * reconciliation, customer support, and regulatory reporting.\n *\n * Sign convention:\n * - positive `delta` — credit (merchant award, refund, manual top-up)\n * - negative `delta` — debit (mint confirmation against the off-chain\n * balance; `txHash` references the on-chain Mint event)\n */\n@Entity({ name: \"ledger_journal\" })\n@Index([\"userAddress\", \"createdAt\"])\nexport class LedgerJournalEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"delta\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n delta!: bigint;\n\n @Column({ name: \"reason\", type: \"varchar\", length: 128 })\n reason!: string;\n\n @Column({ name: \"tx_hash\", type: \"varchar\", length: 66, nullable: true })\n txHash?: string | null;\n\n @CreateDateColumn({ name: \"created_at\" })\n createdAt!: Date;\n}\n","import { Column, Entity, PrimaryColumn, UpdateDateColumn } from \"typeorm\";\n\n/**\n * Persistent cursor for `PointIndexer` / `BurnIndexer`. Multiple rows\n * coexist keyed by `id` (e.g. `default` for the mint indexer,\n * `burn:0x...` for each per-token burn indexer).\n *\n * Stores the **next** block to scan, not the last processed one.\n * Indexer reads on startup and resumes from there.\n */\n@Entity({ name: \"indexer_cursors\" })\nexport class IndexerCursorEntity {\n @PrimaryColumn({ type: \"varchar\", length: 64 })\n id!: string;\n\n @Column({\n name: \"next_block\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n nextBlock!: bigint;\n\n @UpdateDateColumn({ name: \"updated_at\" })\n updatedAt!: Date;\n}\n"],"mappings":"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","names":["import_typeorm","import_typeorm","import_typeorm","import_typeorm"]}
1
+ {"version":3,"sources":["../../src/entities/index.ts","../../src/entities/locked-mint.entity.ts","../../src/entities/pending-credit.entity.ts","../../src/entities/user-balance.entity.ts","../../src/entities/ledger-journal.entity.ts","../../src/entities/indexer-cursor.entity.ts","../../src/entities/redemption-history.entity.ts"],"sourcesContent":["export { LockedMintEntity } from \"./locked-mint.entity\";\nexport {\n PendingCreditEntity,\n type PendingCreditStatus,\n} from \"./pending-credit.entity\";\nexport { UserBalanceEntity } from \"./user-balance.entity\";\nexport { LedgerJournalEntity } from \"./ledger-journal.entity\";\nexport { IndexerCursorEntity } from \"./indexer-cursor.entity\";\nexport { RedemptionHistoryEntity } from \"./redemption-history.entity\";\n\nimport { LockedMintEntity } from \"./locked-mint.entity\";\nimport { PendingCreditEntity } from \"./pending-credit.entity\";\nimport { UserBalanceEntity } from \"./user-balance.entity\";\nimport { LedgerJournalEntity } from \"./ledger-journal.entity\";\nimport { IndexerCursorEntity } from \"./indexer-cursor.entity\";\nimport { RedemptionHistoryEntity } from \"./redemption-history.entity\";\n\n/**\n * All entities in one array — drop into TypeORM's `entities` config or\n * NestJS's `TypeOrmModule.forFeature(PAFI_ENTITIES)`.\n */\nexport const PAFI_ENTITIES = [\n LockedMintEntity,\n PendingCreditEntity,\n UserBalanceEntity,\n LedgerJournalEntity,\n IndexerCursorEntity,\n RedemptionHistoryEntity,\n] as const;\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\nimport type { MintingStatus } from \"@pafi-dev/issuer\";\n\n/**\n * A reservation against a user's off-chain balance.\n *\n * Lifecycle:\n * PENDING ── PointIndexer matches Mint event ──▶ MINTED\n * │\n * ├── deadline elapsed ────────────────────▶ EXPIRED\n * │\n * └── tx reverted ─────────────────────────▶ FAILED\n *\n * Index policy — the three composite indexes below mirror the three\n * query shapes the SDK runs against this table. They are the source\n * of truth for the schema; the migrations create indexes with the\n * exact same names so `synchronize: false` deployments do not drift.\n *\n * IDX_locked_mint_user_token_status_expires\n * Hot path: `sumPendingLocks` inside `getBalance` + `lockForMinting`.\n * 4-col covering index — `expires_at` in the range scan instead\n * of post-filtered from the heap.\n *\n * IDX_locked_mint_user_token_amount_status\n * Hot path: `PointIndexer.pickMatchingLock` + the lock-resolution\n * `findOne` inside `deductBalance`. `amount` is the\n * selectivity-critical predicate.\n *\n * IDX_locked_mint_pending_expires\n * Sweep path: `markExpiredLocks` UPDATE. Partial\n * `WHERE status = 'PENDING'` keeps the index small — only the\n * rows the sweep can touch live in the index.\n *\n * IDX_locked_mint_user_op_hash\n * Bundler-receipt fallback in `statusHandlers` — point lookup\n * by userOpHash.\n */\n@Entity({ name: \"locked_mint_requests\" })\n@Index(\"IDX_locked_mint_user_token_status_expires\", [\n \"userAddress\",\n \"tokenAddress\",\n \"status\",\n \"expiresAt\",\n])\n@Index(\"IDX_locked_mint_user_token_amount_status\", [\n \"userAddress\",\n \"tokenAddress\",\n \"amount\",\n \"status\",\n])\n@Index(\"IDX_locked_mint_pending_expires\", [\"expiresAt\"], {\n where: \"\\\"status\\\" = 'PENDING'\",\n})\n@Index(\"IDX_locked_mint_user_op_hash\", [\"userOpHash\"])\nexport class LockedMintEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"amount\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n amount!: bigint;\n\n @Column({\n name: \"status\",\n type: \"varchar\",\n length: 16,\n default: \"PENDING\",\n })\n status!: MintingStatus;\n\n @CreateDateColumn({ name: \"created_at\" })\n createdAt!: Date;\n\n @Column({ name: \"expires_at\", type: \"timestamp with time zone\" })\n expiresAt!: Date;\n\n @Column({ name: \"tx_hash\", type: \"varchar\", length: 66, nullable: true })\n txHash?: string | null;\n\n /**\n * ERC-4337 userOpHash returned by the bundler at /claim/submit.\n * Bound to the lock so `/claim/status` can fall back to the bundler\n * receipt — required when multiple PENDING locks share the same\n * `amount` (PointIndexer matches by amount and can pick the wrong\n * sibling lock).\n */\n @Column({\n name: \"user_op_hash\",\n type: \"varchar\",\n length: 66,\n nullable: true,\n })\n userOpHash?: string | null;\n}\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\n\nexport type PendingCreditStatus = \"PENDING\" | \"RESOLVED\" | \"EXPIRED\";\n\n/**\n * Reverse flow — user burns on-chain PT, `BurnIndexer` observes\n * `Transfer(user → 0x0)`, the credit is settled to the off-chain ledger.\n *\n * Lifecycle:\n * PENDING ── Burn tx observed by indexer ──▶ RESOLVED\n * │\n * └── deadline elapsed ────────────────▶ EXPIRED\n *\n * The credit is reserved BEFORE the UserOp is submitted so the\n * indexer can correlate `(user, amount, token)` back to the off-chain\n * row when the burn lands.\n */\n@Entity({ name: \"pending_credits\" })\n@Index([\"userAddress\", \"status\"])\n@Index([\"txHash\"])\nexport class PendingCreditEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"amount\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n amount!: bigint;\n\n @Column({\n name: \"status\",\n type: \"varchar\",\n length: 16,\n default: \"PENDING\",\n })\n status!: PendingCreditStatus;\n\n /** On-chain burn tx that settled this credit. Null until indexer resolves. */\n @Column({ name: \"tx_hash\", type: \"varchar\", length: 66, nullable: true })\n txHash?: string | null;\n\n /** ERC-4337 userOpHash bound at /redeem/submit. See `LockedMintEntity`. */\n @Column({\n name: \"user_op_hash\",\n type: \"varchar\",\n length: 66,\n nullable: true,\n })\n userOpHash?: string | null;\n\n @CreateDateColumn({ name: \"created_at\" })\n createdAt!: Date;\n\n @Column({ name: \"expires_at\", type: \"timestamp with time zone\" })\n expiresAt!: Date;\n\n @Column({\n name: \"resolved_at\",\n type: \"timestamp with time zone\",\n nullable: true,\n })\n resolvedAt?: Date | null;\n}\n","import { Column, Entity, PrimaryColumn, UpdateDateColumn } from \"typeorm\";\n\n/**\n * Off-chain point balance per `(userAddress, tokenAddress)`.\n *\n * `balance` is the **total** owned; pending reservations live in\n * `LockedMintEntity`. Available balance = total − sum(PENDING locks)\n * — `getBalance` in `PostgresPointLedger` does this subtraction\n * inside a transaction so reads are race-free.\n *\n * All amounts are `numeric(78, 0)` for full bigint precision (uint256\n * fits in 78 decimal digits). TypeORM transforms bigint ↔ string at\n * the boundary; in JS/TS code we always deal with `bigint`.\n */\n@Entity({ name: \"user_balances\" })\nexport class UserBalanceEntity {\n @PrimaryColumn({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @PrimaryColumn({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"balance\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n default: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n balance!: bigint;\n\n @UpdateDateColumn({ name: \"updated_at\" })\n updatedAt!: Date;\n}\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\n\n/**\n * Append-only audit trail for every balance mutation. Used for\n * reconciliation, customer support, and regulatory reporting.\n *\n * Sign convention:\n * - positive `delta` — credit (merchant award, refund, manual top-up)\n * - negative `delta` — debit (mint confirmation against the off-chain\n * balance; `txHash` references the on-chain Mint event)\n *\n * Idempotency invariant: for any indexer-driven reason (e.g.\n * `MINT_CONFIRMED`, `BURN_FOR_CREDIT`) the tuple\n * `(user_address, token_address, tx_hash, reason)` is unique. Enforced\n * by a partial unique index that only fires when `tx_hash IS NOT NULL`,\n * so off-chain reasons (e.g. `AIRDROP`) without a tx hash are\n * unaffected. Last-line defense against double-deduct / double-credit\n * when the indexer replays an event (reorg, restart, duplicate pod).\n *\n * The `token_address` column is part of the tuple to prevent the\n * audit PACI5-7 collapse: a single transaction that mints two\n * different PointTokens for the same user (batched EIP-7702 claim or\n * Pimlico bundler grouping two single-mint UserOps into one bundle)\n * would otherwise have its second debit silently shadowed by the\n * first token's journal row, while `PointIndexer.finalize` still\n * flipped the second lock to MINTED — silent off-chain debit miss.\n */\n@Entity({ name: \"ledger_journal\" })\n@Index([\"userAddress\", \"createdAt\"])\n@Index(\n \"UQ_ledger_journal_user_token_tx_reason\",\n [\"userAddress\", \"tokenAddress\", \"txHash\", \"reason\"],\n {\n unique: true,\n where: '\"tx_hash\" IS NOT NULL',\n },\n)\nexport class LedgerJournalEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42 })\n tokenAddress!: string;\n\n @Column({\n name: \"delta\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n delta!: bigint;\n\n @Column({ name: \"reason\", type: \"varchar\", length: 128 })\n reason!: string;\n\n @Column({ name: \"tx_hash\", type: \"varchar\", length: 66, nullable: true })\n txHash?: string | null;\n\n @CreateDateColumn({ name: \"created_at\" })\n createdAt!: Date;\n}\n","import { Column, Entity, PrimaryColumn, UpdateDateColumn } from \"typeorm\";\n\n/**\n * Persistent cursor for `PointIndexer` / `BurnIndexer`. Multiple rows\n * coexist keyed by `id` (e.g. `default` for the mint indexer,\n * `burn:0x...` for each per-token burn indexer).\n *\n * Stores the **next** block to scan, not the last processed one.\n * Indexer reads on startup and resumes from there.\n */\n@Entity({ name: \"indexer_cursors\" })\nexport class IndexerCursorEntity {\n @PrimaryColumn({ type: \"varchar\", length: 64 })\n id!: string;\n\n @Column({\n name: \"next_block\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n nextBlock!: bigint;\n\n @UpdateDateColumn({ name: \"updated_at\" })\n updatedAt!: Date;\n}\n","import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n PrimaryGeneratedColumn,\n} from \"typeorm\";\n\n/**\n * Per-user redemption history row. One row per successful initiate\n * (call to `RedemptionService.recordSuccessfulInitiate`).\n *\n * `sumRedeemedSince` does a SUM(amount) WHERE created_at >= :since\n * which uses the (user_address, created_at) composite index. We do\n * NOT prune old rows automatically — they're cheap and useful for\n * audit. Issuers can add a periodic VACUUM/partition policy if the\n * table grows past ~100M rows.\n *\n * Amounts are `numeric(78, 0)` for full bigint precision.\n */\n@Entity({ name: \"redemption_history\" })\n@Index(\"idx_redemption_history_user_created\", [\n \"userAddress\",\n \"createdAtUnixSec\",\n])\nexport class RedemptionHistoryEntity {\n @PrimaryGeneratedColumn(\"uuid\")\n id!: string;\n\n @Column({ name: \"user_address\", type: \"varchar\", length: 42 })\n userAddress!: string;\n\n @Column({ name: \"token_address\", type: \"varchar\", length: 42, nullable: true })\n tokenAddress!: string | null;\n\n @Column({\n name: \"amount_pt\",\n type: \"numeric\",\n precision: 78,\n scale: 0,\n transformer: {\n to: (value: bigint) => value.toString(),\n from: (value: string) => BigInt(value),\n },\n })\n amountPt!: bigint;\n\n /**\n * Caller-controlled timestamp (unix seconds). Stored as integer, not\n * timestamptz, because the evaluator works in unix seconds and we want\n * the same time domain on read + write — no surprise tz conversions.\n */\n @Column({ name: \"created_at_unix_sec\", type: \"bigint\" })\n createdAtUnixSec!: string;\n\n /**\n * Optional pointer back to the burn-flow reservation (PendingCredit.id).\n * Lets ops trace a redemption-history row to the underlying lock.\n */\n @Column({\n name: \"reservation_id\",\n type: \"varchar\",\n length: 64,\n nullable: true,\n })\n reservationId!: string | null;\n\n @CreateDateColumn({ name: \"row_created_at\", type: \"timestamptz\" })\n rowCreatedAt!: Date;\n}\n"],"mappings":"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","names":["import_typeorm","import_typeorm","import_typeorm","import_typeorm","import_typeorm"]}
@@ -10,8 +10,29 @@ import { MintingStatus } from '@pafi-dev/issuer';
10
10
  * │
11
11
  * └── tx reverted ─────────────────────────▶ FAILED
12
12
  *
13
- * The `(userAddress, status)` composite index keeps the "sum all
14
- * PENDING locks" hot path of `lockForMinting()` fast under load.
13
+ * Index policy the three composite indexes below mirror the three
14
+ * query shapes the SDK runs against this table. They are the source
15
+ * of truth for the schema; the migrations create indexes with the
16
+ * exact same names so `synchronize: false` deployments do not drift.
17
+ *
18
+ * IDX_locked_mint_user_token_status_expires
19
+ * Hot path: `sumPendingLocks` inside `getBalance` + `lockForMinting`.
20
+ * 4-col covering index — `expires_at` in the range scan instead
21
+ * of post-filtered from the heap.
22
+ *
23
+ * IDX_locked_mint_user_token_amount_status
24
+ * Hot path: `PointIndexer.pickMatchingLock` + the lock-resolution
25
+ * `findOne` inside `deductBalance`. `amount` is the
26
+ * selectivity-critical predicate.
27
+ *
28
+ * IDX_locked_mint_pending_expires
29
+ * Sweep path: `markExpiredLocks` UPDATE. Partial
30
+ * `WHERE status = 'PENDING'` keeps the index small — only the
31
+ * rows the sweep can touch live in the index.
32
+ *
33
+ * IDX_locked_mint_user_op_hash
34
+ * Bundler-receipt fallback in `statusHandlers` — point lookup
35
+ * by userOpHash.
15
36
  */
16
37
  declare class LockedMintEntity {
17
38
  id: string;
@@ -88,6 +109,22 @@ declare class UserBalanceEntity {
88
109
  * - positive `delta` — credit (merchant award, refund, manual top-up)
89
110
  * - negative `delta` — debit (mint confirmation against the off-chain
90
111
  * balance; `txHash` references the on-chain Mint event)
112
+ *
113
+ * Idempotency invariant: for any indexer-driven reason (e.g.
114
+ * `MINT_CONFIRMED`, `BURN_FOR_CREDIT`) the tuple
115
+ * `(user_address, token_address, tx_hash, reason)` is unique. Enforced
116
+ * by a partial unique index that only fires when `tx_hash IS NOT NULL`,
117
+ * so off-chain reasons (e.g. `AIRDROP`) without a tx hash are
118
+ * unaffected. Last-line defense against double-deduct / double-credit
119
+ * when the indexer replays an event (reorg, restart, duplicate pod).
120
+ *
121
+ * The `token_address` column is part of the tuple to prevent the
122
+ * audit PACI5-7 collapse: a single transaction that mints two
123
+ * different PointTokens for the same user (batched EIP-7702 claim or
124
+ * Pimlico bundler grouping two single-mint UserOps into one bundle)
125
+ * would otherwise have its second debit silently shadowed by the
126
+ * first token's journal row, while `PointIndexer.finalize` still
127
+ * flipped the second lock to MINTED — silent off-chain debit miss.
91
128
  */
92
129
  declare class LedgerJournalEntity {
93
130
  id: string;
@@ -113,10 +150,41 @@ declare class IndexerCursorEntity {
113
150
  updatedAt: Date;
114
151
  }
115
152
 
153
+ /**
154
+ * Per-user redemption history row. One row per successful initiate
155
+ * (call to `RedemptionService.recordSuccessfulInitiate`).
156
+ *
157
+ * `sumRedeemedSince` does a SUM(amount) WHERE created_at >= :since
158
+ * which uses the (user_address, created_at) composite index. We do
159
+ * NOT prune old rows automatically — they're cheap and useful for
160
+ * audit. Issuers can add a periodic VACUUM/partition policy if the
161
+ * table grows past ~100M rows.
162
+ *
163
+ * Amounts are `numeric(78, 0)` for full bigint precision.
164
+ */
165
+ declare class RedemptionHistoryEntity {
166
+ id: string;
167
+ userAddress: string;
168
+ tokenAddress: string | null;
169
+ amountPt: bigint;
170
+ /**
171
+ * Caller-controlled timestamp (unix seconds). Stored as integer, not
172
+ * timestamptz, because the evaluator works in unix seconds and we want
173
+ * the same time domain on read + write — no surprise tz conversions.
174
+ */
175
+ createdAtUnixSec: string;
176
+ /**
177
+ * Optional pointer back to the burn-flow reservation (PendingCredit.id).
178
+ * Lets ops trace a redemption-history row to the underlying lock.
179
+ */
180
+ reservationId: string | null;
181
+ rowCreatedAt: Date;
182
+ }
183
+
116
184
  /**
117
185
  * All entities in one array — drop into TypeORM's `entities` config or
118
186
  * NestJS's `TypeOrmModule.forFeature(PAFI_ENTITIES)`.
119
187
  */
120
- declare const PAFI_ENTITIES: readonly [typeof LockedMintEntity, typeof PendingCreditEntity, typeof UserBalanceEntity, typeof LedgerJournalEntity, typeof IndexerCursorEntity];
188
+ declare const PAFI_ENTITIES: readonly [typeof LockedMintEntity, typeof PendingCreditEntity, typeof UserBalanceEntity, typeof LedgerJournalEntity, typeof IndexerCursorEntity, typeof RedemptionHistoryEntity];
121
189
 
122
- export { IndexerCursorEntity, LedgerJournalEntity, LockedMintEntity, PAFI_ENTITIES, PendingCreditEntity, type PendingCreditStatus, UserBalanceEntity };
190
+ export { IndexerCursorEntity, LedgerJournalEntity, LockedMintEntity, PAFI_ENTITIES, PendingCreditEntity, type PendingCreditStatus, RedemptionHistoryEntity, UserBalanceEntity };
@@ -10,8 +10,29 @@ import { MintingStatus } from '@pafi-dev/issuer';
10
10
  * │
11
11
  * └── tx reverted ─────────────────────────▶ FAILED
12
12
  *
13
- * The `(userAddress, status)` composite index keeps the "sum all
14
- * PENDING locks" hot path of `lockForMinting()` fast under load.
13
+ * Index policy the three composite indexes below mirror the three
14
+ * query shapes the SDK runs against this table. They are the source
15
+ * of truth for the schema; the migrations create indexes with the
16
+ * exact same names so `synchronize: false` deployments do not drift.
17
+ *
18
+ * IDX_locked_mint_user_token_status_expires
19
+ * Hot path: `sumPendingLocks` inside `getBalance` + `lockForMinting`.
20
+ * 4-col covering index — `expires_at` in the range scan instead
21
+ * of post-filtered from the heap.
22
+ *
23
+ * IDX_locked_mint_user_token_amount_status
24
+ * Hot path: `PointIndexer.pickMatchingLock` + the lock-resolution
25
+ * `findOne` inside `deductBalance`. `amount` is the
26
+ * selectivity-critical predicate.
27
+ *
28
+ * IDX_locked_mint_pending_expires
29
+ * Sweep path: `markExpiredLocks` UPDATE. Partial
30
+ * `WHERE status = 'PENDING'` keeps the index small — only the
31
+ * rows the sweep can touch live in the index.
32
+ *
33
+ * IDX_locked_mint_user_op_hash
34
+ * Bundler-receipt fallback in `statusHandlers` — point lookup
35
+ * by userOpHash.
15
36
  */
16
37
  declare class LockedMintEntity {
17
38
  id: string;
@@ -88,6 +109,22 @@ declare class UserBalanceEntity {
88
109
  * - positive `delta` — credit (merchant award, refund, manual top-up)
89
110
  * - negative `delta` — debit (mint confirmation against the off-chain
90
111
  * balance; `txHash` references the on-chain Mint event)
112
+ *
113
+ * Idempotency invariant: for any indexer-driven reason (e.g.
114
+ * `MINT_CONFIRMED`, `BURN_FOR_CREDIT`) the tuple
115
+ * `(user_address, token_address, tx_hash, reason)` is unique. Enforced
116
+ * by a partial unique index that only fires when `tx_hash IS NOT NULL`,
117
+ * so off-chain reasons (e.g. `AIRDROP`) without a tx hash are
118
+ * unaffected. Last-line defense against double-deduct / double-credit
119
+ * when the indexer replays an event (reorg, restart, duplicate pod).
120
+ *
121
+ * The `token_address` column is part of the tuple to prevent the
122
+ * audit PACI5-7 collapse: a single transaction that mints two
123
+ * different PointTokens for the same user (batched EIP-7702 claim or
124
+ * Pimlico bundler grouping two single-mint UserOps into one bundle)
125
+ * would otherwise have its second debit silently shadowed by the
126
+ * first token's journal row, while `PointIndexer.finalize` still
127
+ * flipped the second lock to MINTED — silent off-chain debit miss.
91
128
  */
92
129
  declare class LedgerJournalEntity {
93
130
  id: string;
@@ -113,10 +150,41 @@ declare class IndexerCursorEntity {
113
150
  updatedAt: Date;
114
151
  }
115
152
 
153
+ /**
154
+ * Per-user redemption history row. One row per successful initiate
155
+ * (call to `RedemptionService.recordSuccessfulInitiate`).
156
+ *
157
+ * `sumRedeemedSince` does a SUM(amount) WHERE created_at >= :since
158
+ * which uses the (user_address, created_at) composite index. We do
159
+ * NOT prune old rows automatically — they're cheap and useful for
160
+ * audit. Issuers can add a periodic VACUUM/partition policy if the
161
+ * table grows past ~100M rows.
162
+ *
163
+ * Amounts are `numeric(78, 0)` for full bigint precision.
164
+ */
165
+ declare class RedemptionHistoryEntity {
166
+ id: string;
167
+ userAddress: string;
168
+ tokenAddress: string | null;
169
+ amountPt: bigint;
170
+ /**
171
+ * Caller-controlled timestamp (unix seconds). Stored as integer, not
172
+ * timestamptz, because the evaluator works in unix seconds and we want
173
+ * the same time domain on read + write — no surprise tz conversions.
174
+ */
175
+ createdAtUnixSec: string;
176
+ /**
177
+ * Optional pointer back to the burn-flow reservation (PendingCredit.id).
178
+ * Lets ops trace a redemption-history row to the underlying lock.
179
+ */
180
+ reservationId: string | null;
181
+ rowCreatedAt: Date;
182
+ }
183
+
116
184
  /**
117
185
  * All entities in one array — drop into TypeORM's `entities` config or
118
186
  * NestJS's `TypeOrmModule.forFeature(PAFI_ENTITIES)`.
119
187
  */
120
- declare const PAFI_ENTITIES: readonly [typeof LockedMintEntity, typeof PendingCreditEntity, typeof UserBalanceEntity, typeof LedgerJournalEntity, typeof IndexerCursorEntity];
188
+ declare const PAFI_ENTITIES: readonly [typeof LockedMintEntity, typeof PendingCreditEntity, typeof UserBalanceEntity, typeof LedgerJournalEntity, typeof IndexerCursorEntity, typeof RedemptionHistoryEntity];
121
189
 
122
- export { IndexerCursorEntity, LedgerJournalEntity, LockedMintEntity, PAFI_ENTITIES, PendingCreditEntity, type PendingCreditStatus, UserBalanceEntity };
190
+ export { IndexerCursorEntity, LedgerJournalEntity, LockedMintEntity, PAFI_ENTITIES, PendingCreditEntity, type PendingCreditStatus, RedemptionHistoryEntity, UserBalanceEntity };