@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/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
|
|
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.
|
|
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
|
package/dist/entities/index.cjs
CHANGED
|
@@ -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)(
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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 };
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -10,8 +10,29 @@ import { MintingStatus } from '@pafi-dev/issuer';
|
|
|
10
10
|
* │
|
|
11
11
|
* └── tx reverted ─────────────────────────▶ FAILED
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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 };
|