@pafi-dev/issuer-postgres 0.2.0 → 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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/postgresPointLedger.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/postgresCursorStore.ts","../src/postgresRedemptionHistoryStore.ts","../src/entities/redemption-history.entity.ts","../src/entities/index.ts","../src/migrations/1700000000000-InitialSchema.ts","../src/migrations/1746230400001-CreateRedemptionHistory.ts","../src/migrations/index.ts"],"sourcesContent":["import type { DataSource, EntityManager } from \"typeorm\";\nimport { getAddress, type Address, type Hex } from \"viem\";\nimport type {\n IPointLedger,\n LockedMintRequest,\n MintingStatus,\n PendingCredit,\n} from \"@pafi-dev/issuer\";\n\nimport { LockedMintEntity } from \"./entities/locked-mint.entity\";\nimport { PendingCreditEntity } from \"./entities/pending-credit.entity\";\nimport { UserBalanceEntity } from \"./entities/user-balance.entity\";\nimport { LedgerJournalEntity } from \"./entities/ledger-journal.entity\";\n\n/**\n * Postgres SQLSTATE codes that indicate a transient transaction conflict\n * which is safe to retry. `40P01` = deadlock detected;\n * `40001` = serialization failure (only seen at SERIALIZABLE isolation\n * but harmless to retry at READ COMMITTED too).\n */\nconst RETRIABLE_PG_CODES = new Set([\"40P01\", \"40001\"]);\n\ninterface PgError {\n code?: string;\n message?: string;\n}\n\nfunction isRetriablePgError(err: unknown): boolean {\n const e = err as PgError | undefined;\n if (!e) return false;\n if (e.code && RETRIABLE_PG_CODES.has(e.code)) return true;\n // Some drivers attach the code on a nested cause / driverError. Defensive:\n // string-match the message as a last resort.\n if (e.message && /deadlock detected|could not serialize/i.test(e.message)) {\n return true;\n }\n return false;\n}\n\n/**\n * Wrap a transaction body in a deadlock-retry loop. Postgres can raise\n * `40P01` when concurrent transactions take row locks in incompatible\n * orders — once `lockForMinting`/`deductBalance` use `FOR UPDATE`,\n * deadlocks become a normal occurrence under contention. Without retry\n * the issuer surfaces them as 500s instead of absorbing them transparently.\n */\nasync function withDeadlockRetry<T>(\n dataSource: DataSource,\n fn: (tx: EntityManager) => Promise<T>,\n maxAttempts = 3,\n): Promise<T> {\n let attempt = 0;\n let delayMs = 25;\n for (;;) {\n attempt++;\n try {\n return await dataSource.transaction(fn);\n } catch (err) {\n if (attempt >= maxAttempts || !isRetriablePgError(err)) {\n throw err;\n }\n // Exponential backoff with jitter. 25ms → 50ms → 100ms typical.\n await new Promise((r) =>\n setTimeout(r, delayMs + Math.floor(Math.random() * delayMs)),\n );\n delayMs *= 2;\n }\n }\n}\n\nexport interface PostgresPointLedgerOptions {\n /**\n * Optional logger. When omitted, the service is silent. Pass a Nest\n * `Logger` / pino instance / `console` to surface debug + info\n * lines.\n */\n logger?: {\n debug?: (msg: string) => void;\n log?: (msg: string) => void;\n warn?: (msg: string) => void;\n };\n}\n\n/**\n * Postgres-backed `IPointLedger` — the reference impl every issuer\n * starts from. Framework-agnostic: takes a TypeORM `DataSource` in the\n * constructor, no NestJS decorators or DI tokens. Wrap in your\n * favorite injector.\n *\n * Implements every required + optional method from `IPointLedger`,\n * including the v1.4 reverse flow (`reservePendingCredit`,\n * `resolveCreditByBurnTx`) and the bundler-receipt fallback hooks\n * (`bindMintUserOpHash`, `bindCreditUserOpHash`, `getMintLock`,\n * `getPendingCredit`).\n *\n * Multi-token: every method requires `tokenAddress` — there is no\n * \"default token\" bucket. Single-token issuers pass the same address\n * everywhere.\n */\nexport class PostgresPointLedger implements IPointLedger {\n private readonly logger: PostgresPointLedgerOptions[\"logger\"];\n\n constructor(\n private readonly dataSource: DataSource,\n options: PostgresPointLedgerOptions = {},\n ) {\n this.logger = options.logger;\n }\n\n // ---------------------------------------------------------------------\n // Read\n // ---------------------------------------------------------------------\n\n async getBalance(\n userAddress: Address,\n tokenAddress?: Address,\n ): Promise<bigint> {\n const { user, token } = normalize(userAddress, tokenAddress);\n\n // Pure read — `sumPendingLocks` filters `expires_at > now`, so\n // expired locks are naturally excluded without writing a status\n // update. The lock table grows monotonically until a periodic\n // background sweep runs `markExpiredLocks()` (see below). The\n // read query stays cheap because it filters by the composite\n // index `(user_address, token_address, status, expires_at)`.\n const [balanceRow, locked] = await Promise.all([\n this.dataSource\n .getRepository(UserBalanceEntity)\n .findOne({ where: { userAddress: user, tokenAddress: token } }),\n this.sumPendingLocks(user, token),\n ]);\n const total = balanceRow?.balance ?? 0n;\n const available = total - locked;\n return available < 0n ? 0n : available;\n }\n\n /**\n * Background sweep — marks all expired PENDING locks as EXPIRED in\n * a single UPDATE. Issuers SHOULD call this periodically (e.g.\n * every 1-5 minutes via a cron / NestJS `@Interval`) to keep the\n * lock table from growing unbounded.\n *\n * A single sweep amortizes the write cost vs `getBalance` doing a\n * wide UPDATE on every read. Returns the number of rows transitioned.\n *\n * @example\n * ```ts\n * import { Interval } from \"@nestjs/schedule\";\n *\n * @Injectable()\n * export class LockSweepService {\n * constructor(private readonly ledger: PostgresPointLedger) {}\n *\n * @Interval(5 * 60 * 1000) // 5 minutes\n * async sweep() {\n * const swept = await this.ledger.markExpiredLocks();\n * this.logger.debug(`expired ${swept} mint locks`);\n * }\n * }\n * ```\n */\n async markExpiredLocks(): Promise<number> {\n const result = await this.dataSource\n .getRepository(LockedMintEntity)\n .createQueryBuilder()\n .update()\n .set({ status: \"EXPIRED\" })\n .where(\"status = :pending\", { pending: \"PENDING\" })\n .andWhere(\"expires_at <= :now\", { now: new Date() })\n .execute();\n const swept = result.affected ?? 0;\n if (swept > 0) {\n this.logger?.debug?.(`markExpiredLocks: swept ${swept} mint locks`);\n }\n return swept;\n }\n\n async getLockedRequests(\n userAddress: Address,\n tokenAddress?: Address,\n ): Promise<LockedMintRequest[]> {\n const { user, token } = normalize(userAddress, tokenAddress);\n const rows = await this.dataSource\n .getRepository(LockedMintEntity)\n .find({\n where: { userAddress: user, tokenAddress: token, status: \"PENDING\" },\n order: { createdAt: \"ASC\" },\n });\n return rows.map((row) => this.toSdkLock(row));\n }\n\n async getMintLock(\n lockId: string,\n userAddress?: Address,\n ): Promise<LockedMintRequest | null> {\n const row = await this.dataSource\n .getRepository(LockedMintEntity)\n .findOne({ where: { id: lockId } });\n if (!row) return null;\n if (\n userAddress &&\n row.userAddress.toLowerCase() !== userAddress.toLowerCase()\n ) {\n return null;\n }\n return this.toSdkLock(row);\n }\n\n /** Raw TypeORM row — escape hatch for callers that need entity fields. */\n async getMintLockEntity(lockId: string): Promise<LockedMintEntity | null> {\n return this.dataSource\n .getRepository(LockedMintEntity)\n .findOne({ where: { id: lockId } });\n }\n\n async getPendingCredit(\n lockId: string,\n userAddress?: Address,\n ): Promise<PendingCredit | null> {\n const row = await this.dataSource\n .getRepository(PendingCreditEntity)\n .findOne({ where: { id: lockId } });\n if (!row) return null;\n if (\n userAddress &&\n row.userAddress.toLowerCase() !== userAddress.toLowerCase()\n ) {\n return null;\n }\n return {\n lockId: row.id,\n userAddress: getAddress(row.userAddress) as Address,\n amount: row.amount,\n tokenAddress: row.tokenAddress\n ? (getAddress(row.tokenAddress) as Address)\n : undefined,\n status: row.status as PendingCredit[\"status\"],\n createdAt: row.createdAt.getTime(),\n expiresAt: row.expiresAt.getTime(),\n txHash: (row.txHash as Hex | null) ?? undefined,\n resolvedAt: row.resolvedAt?.getTime(),\n userOpHash: (row.userOpHash as Hex | null) ?? undefined,\n };\n }\n\n /** Raw TypeORM row escape hatch for credits. */\n async getPendingCreditEntity(\n lockId: string,\n ): Promise<PendingCreditEntity | null> {\n return this.dataSource\n .getRepository(PendingCreditEntity)\n .findOne({ where: { id: lockId } });\n }\n\n /**\n * Paginated list of a user's mint requests across all statuses and\n * all tokens — used by `GET /user/transactions` reference endpoint.\n */\n async listUserTransactions(\n userAddress: Address,\n limit: number,\n offset: number,\n ): Promise<{ rows: LockedMintEntity[]; total: number }> {\n const user = getAddress(userAddress);\n const [rows, total] = await this.dataSource\n .getRepository(LockedMintEntity)\n .findAndCount({\n where: { userAddress: user },\n order: { createdAt: \"DESC\" },\n take: limit,\n skip: offset,\n });\n return { rows, total };\n }\n\n // ---------------------------------------------------------------------\n // Write\n // ---------------------------------------------------------------------\n\n async creditBalance(\n userAddress: Address,\n amount: bigint,\n reason: string,\n tokenAddress?: Address,\n ): Promise<void> {\n if (amount <= 0n) {\n throw new Error(\"creditBalance: amount must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n await withDeadlockRetry(this.dataSource, async (tx) => {\n // FOR UPDATE on the existing balance row — avoids lost-update under\n // concurrent credits (each tx reads stale total + writes incremented\n // value at READ COMMITTED, dropping increments). UPSERT cannot lock\n // a row that doesn't exist yet, so first-credit case relies on the\n // unique constraint (user_address, token_address) to serialize.\n const existing = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n const next = (existing?.balance ?? 0n) + amount;\n\n await tx\n .getRepository(UserBalanceEntity)\n .upsert(\n { userAddress: user, tokenAddress: token, balance: next },\n { conflictPaths: [\"userAddress\", \"tokenAddress\"] },\n );\n\n await tx.getRepository(LedgerJournalEntity).insert({\n userAddress: user,\n tokenAddress: token,\n delta: amount,\n reason,\n });\n });\n\n this.logger?.debug?.(`credit ${user}[${token}] +${amount} (${reason})`);\n }\n\n async lockForMinting(\n userAddress: Address,\n amount: bigint,\n lockDurationMs: number,\n tokenAddress?: Address,\n ): Promise<string> {\n if (amount <= 0n) {\n throw new Error(\"lockForMinting: amount must be positive\");\n }\n if (lockDurationMs <= 0) {\n throw new Error(\"lockForMinting: lockDurationMs must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n return withDeadlockRetry(this.dataSource, async (tx) => {\n // SELECT … FOR UPDATE on the user's balance row to prevent TOCTOU.\n // Without this, two concurrent lockForMinting() at READ COMMITTED can\n // both see the same `available` and both insert lock rows, allowing\n // the issuer to over-commit ledger balance against on-chain mints.\n const balanceRow = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n const total = balanceRow?.balance ?? 0n;\n\n // Sum PENDING locks while holding the balance row lock — any\n // concurrent insert would serialize behind us on the same row.\n const pendingTotal = await tx\n .getRepository(LockedMintEntity)\n .createQueryBuilder(\"lock\")\n .select(\"COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)\", \"sum\")\n .where(\"lock.user_address = :user\", { user })\n .andWhere(\"lock.token_address = :token\", { token })\n .andWhere(\"lock.status = :pending\", { pending: \"PENDING\" })\n .andWhere(\"lock.expires_at > :now\", { now: new Date() })\n .getRawOne<{ sum: string }>();\n\n const locked = pendingTotal ? BigInt(pendingTotal.sum) : 0n;\n const available = total - locked;\n if (available < amount) {\n throw new Error(\n `Insufficient balance: available=${available}, requested=${amount}`,\n );\n }\n\n const lock = await tx.getRepository(LockedMintEntity).save({\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n expiresAt: new Date(Date.now() + lockDurationMs),\n });\n\n this.logger?.debug?.(\n `lock ${lock.id} ${user}[${token}] amount=${amount}`,\n );\n return lock.id;\n });\n }\n\n async releaseLock(lockId: string): Promise<void> {\n const result = await this.dataSource\n .getRepository(LockedMintEntity)\n .delete({ id: lockId, status: \"PENDING\" });\n\n if ((result.affected ?? 0) > 0) {\n this.logger?.debug?.(`release lock ${lockId}`);\n }\n }\n\n async deductBalance(\n userAddress: Address,\n amount: bigint,\n txHash: Hex,\n tokenAddress?: Address,\n ): Promise<void> {\n if (amount <= 0n) {\n throw new Error(\"deductBalance: amount must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n await withDeadlockRetry(this.dataSource, async (tx) => {\n // SELECT … FOR UPDATE — same TOCTOU concern as lockForMinting.\n // Concurrent deductBalance() (e.g. chain reorg + duplicate finalize\n // event) at READ COMMITTED could both pass the `balance >= amount`\n // check and produce a negative balance.\n const balance = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n if (!balance || balance.balance < amount) {\n throw new Error(\n `Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`,\n );\n }\n\n await tx.getRepository(UserBalanceEntity).update(\n { userAddress: user, tokenAddress: token },\n { balance: balance.balance - amount },\n );\n\n await tx.getRepository(LedgerJournalEntity).insert({\n userAddress: user,\n tokenAddress: token,\n delta: -amount,\n reason: \"MINT_CONFIRMED\",\n txHash,\n });\n\n // Resolve the oldest matching PENDING lock atomically.\n const match = await tx.getRepository(LockedMintEntity).findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n },\n order: { createdAt: \"ASC\" },\n });\n if (match) {\n await tx\n .getRepository(LockedMintEntity)\n .update({ id: match.id }, { status: \"MINTED\", txHash });\n }\n });\n\n this.logger?.log?.(`deduct ${user}[${token}] -${amount} tx=${txHash}`);\n }\n\n async updateMintStatus(\n lockId: string,\n status: MintingStatus,\n txHash?: Hex,\n ): Promise<void> {\n const update: Partial<LockedMintEntity> = { status };\n if (txHash) update.txHash = txHash;\n\n await this.dataSource\n .getRepository(LockedMintEntity)\n .update({ id: lockId }, update);\n }\n\n async bindMintUserOpHash(lockId: string, userOpHash: Hex): Promise<void> {\n await this.dataSource\n .getRepository(LockedMintEntity)\n .update({ id: lockId }, { userOpHash });\n }\n\n async bindCreditUserOpHash(lockId: string, userOpHash: Hex): Promise<void> {\n await this.dataSource\n .getRepository(PendingCreditEntity)\n .update({ id: lockId }, { userOpHash });\n }\n\n // ---------------------------------------------------------------------\n // Reverse flow (burn → off-chain credit)\n // ---------------------------------------------------------------------\n\n async reservePendingCredit(\n userAddress: Address,\n amount: bigint,\n durationMs: number,\n tokenAddress?: Address,\n ): Promise<string> {\n if (amount <= 0n) {\n throw new Error(\"reservePendingCredit: amount must be positive\");\n }\n if (durationMs <= 0) {\n throw new Error(\"reservePendingCredit: durationMs must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n const row = await this.dataSource\n .getRepository(PendingCreditEntity)\n .save({\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n expiresAt: new Date(Date.now() + durationMs),\n });\n\n this.logger?.debug?.(\n `reserve pending credit ${row.id} ${user}[${token}] +${amount}`,\n );\n return row.id;\n }\n\n async resolveCreditByBurnTx(lockId: string, txHash: Hex): Promise<void> {\n await withDeadlockRetry(this.dataSource, async (tx) => {\n // FOR UPDATE on the credit row — burn-side mirror of mint resolution.\n // Concurrent calls (chain reorg / duplicate event) at READ COMMITTED\n // could both pass the status check and double-credit balance.\n const credit = await tx\n .getRepository(PendingCreditEntity)\n .createQueryBuilder(\"credit\")\n .setLock(\"pessimistic_write\")\n .where(\"credit.id = :id\", { id: lockId })\n .getOne();\n\n if (!credit) {\n throw new Error(\n `resolveCreditByBurnTx: unknown pending credit ${lockId}`,\n );\n }\n\n if (credit.status === \"RESOLVED\") {\n if (credit.txHash === txHash) return; // idempotent replay\n throw new Error(\n `resolveCreditByBurnTx: credit ${lockId} already resolved with a different txHash`,\n );\n }\n\n if (credit.status === \"EXPIRED\") {\n throw new Error(\n `resolveCreditByBurnTx: credit ${lockId} already expired — burn landed too late`,\n );\n }\n\n const user = credit.userAddress as Address;\n const token = credit.tokenAddress as Address;\n\n // Defense-in-depth — same `txHash` already credited a sibling\n // credit for the same (user, token). Mark this credit resolved\n // without re-applying balance.\n const alreadyResolved = await tx\n .getRepository(PendingCreditEntity)\n .findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n txHash,\n status: \"RESOLVED\",\n },\n });\n if (alreadyResolved) {\n await tx\n .getRepository(PendingCreditEntity)\n .update(\n { id: lockId },\n { status: \"RESOLVED\", txHash, resolvedAt: new Date() },\n );\n return;\n }\n\n // FOR UPDATE on balance row prevents lost-update with concurrent\n // creditBalance / resolveCreditByBurnTx (both increment).\n const balance = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n const next = (balance?.balance ?? 0n) + credit.amount;\n\n await tx\n .getRepository(UserBalanceEntity)\n .upsert(\n { userAddress: user, tokenAddress: token, balance: next },\n { conflictPaths: [\"userAddress\", \"tokenAddress\"] },\n );\n\n await tx.getRepository(LedgerJournalEntity).insert({\n userAddress: user,\n tokenAddress: token,\n delta: credit.amount,\n reason: \"BURN_FOR_CREDIT\",\n txHash,\n });\n\n await tx\n .getRepository(PendingCreditEntity)\n .update(\n { id: lockId },\n { status: \"RESOLVED\", txHash, resolvedAt: new Date() },\n );\n });\n\n this.logger?.log?.(`resolve pending credit ${lockId} tx=${txHash}`);\n }\n\n /**\n * Used by `BurnIndexer.matchLockId` to resolve an on-chain burn\n * event back to a pending credit row. Returns the oldest matching\n * `(user, token, amount, status: PENDING)` lockId, or undefined\n * when no match exists (unsolicited burn — indexer skips).\n */\n async findPendingCreditLockId(\n userAddress: Address,\n amount: bigint,\n tokenAddress: Address,\n ): Promise<string | undefined> {\n const { user, token } = normalize(userAddress, tokenAddress);\n const row = await this.dataSource\n .getRepository(PendingCreditEntity)\n .findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n },\n order: { createdAt: \"ASC\" },\n });\n return row?.id;\n }\n\n // ---------------------------------------------------------------------\n // Internals\n // ---------------------------------------------------------------------\n\n private async sumPendingLocks(\n userAddress: Address,\n tokenAddress: Address,\n ): Promise<bigint> {\n const row = await this.dataSource\n .getRepository(LockedMintEntity)\n .createQueryBuilder(\"lock\")\n .select(\"COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)\", \"sum\")\n .where(\"lock.user_address = :user\", { user: userAddress })\n .andWhere(\"lock.token_address = :token\", { token: tokenAddress })\n .andWhere(\"lock.status = :pending\", { pending: \"PENDING\" })\n .andWhere(\"lock.expires_at > :now\", { now: new Date() })\n .getRawOne<{ sum: string }>();\n return row ? BigInt(row.sum) : 0n;\n }\n\n private toSdkLock(row: LockedMintEntity): LockedMintRequest {\n const out: LockedMintRequest = {\n lockId: row.id,\n userAddress: row.userAddress as Address,\n tokenAddress: row.tokenAddress as Address,\n amount: row.amount,\n status: row.status,\n createdAt: row.createdAt.getTime(),\n expiresAt: row.expiresAt.getTime(),\n };\n if (row.txHash) out.txHash = row.txHash as Hex;\n if (row.userOpHash) out.userOpHash = row.userOpHash as Hex;\n return out;\n }\n}\n\n/**\n * Multi-token guard — throw if `tokenAddress` is missing on any\n * mutating call. Single-token issuers must still pass their token\n * address explicitly so reads + writes never fall into a \"default\"\n * bucket the application never queries.\n */\nfunction normalize(\n userAddress: Address,\n tokenAddress: Address | undefined,\n): { user: Address; token: Address } {\n if (!tokenAddress) {\n throw new Error(\n \"PostgresPointLedger: tokenAddress is required on every call (multi-token ledger)\",\n );\n }\n return {\n user: getAddress(userAddress),\n token: getAddress(tokenAddress),\n };\n}\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","import type { DataSource } from \"typeorm\";\nimport type { IIndexerCursorStore } from \"@pafi-dev/issuer\";\n\nimport { IndexerCursorEntity } from \"./entities/indexer-cursor.entity\";\n\n/**\n * Postgres-backed indexer cursor store. Lets indexers survive\n * restarts — on boot, the indexer reads the last persisted block and\n * resumes scanning from there.\n *\n * Multiple indexers (e.g. `PointIndexer` for Mint events +\n * `BurnIndexer` per token for Transfer→0x0) share the same table via\n * different `cursorId`s. Construct with `cursorId = \"default\"` for\n * the primary mint indexer, or call `forKey(id)` to derive a sibling\n * store for a secondary indexer.\n */\nexport class PostgresCursorStore implements IIndexerCursorStore {\n constructor(\n private readonly dataSource: DataSource,\n private readonly cursorId: string = \"default\",\n ) {}\n\n async load(): Promise<bigint | undefined> {\n const row = await this.dataSource\n .getRepository(IndexerCursorEntity)\n .findOne({ where: { id: this.cursorId } });\n return row?.nextBlock;\n }\n\n async save(blockNumber: bigint): Promise<void> {\n await this.dataSource\n .getRepository(IndexerCursorEntity)\n .upsert(\n { id: this.cursorId, nextBlock: blockNumber },\n { conflictPaths: [\"id\"] },\n );\n }\n\n /** Derived store keyed by a different `id` — for sibling indexers. */\n forKey(cursorId: string): IIndexerCursorStore {\n return new PostgresCursorStore(this.dataSource, cursorId);\n }\n}\n","import type { DataSource } from \"typeorm\";\nimport { getAddress, type Address } from \"viem\";\nimport type { IRedemptionHistoryStore } from \"@pafi-dev/issuer\";\n\nimport { RedemptionHistoryEntity } from \"./entities/redemption-history.entity\";\n\n/**\n * Postgres-backed IRedemptionHistoryStore. Append-only — every\n * `recordRedemption` writes a new row. Reads (`sumRedeemedSince` /\n * `getLastRedeemedAtUnixSec`) hit the (user, time) composite index.\n *\n * Addresses are normalized to checksum form on write and lower-cased\n * on read so the index works regardless of casing inconsistency. The\n * entity stores the canonical (checksummed) form.\n */\nexport class PostgresRedemptionHistoryStore implements IRedemptionHistoryStore {\n constructor(private readonly dataSource: DataSource) {}\n\n async sumRedeemedSince(\n user: Address,\n sinceUnixSec: number,\n pointTokenAddress?: Address,\n ): Promise<bigint> {\n const repo = this.dataSource.getRepository(RedemptionHistoryEntity);\n const qb = repo\n .createQueryBuilder(\"rh\")\n .select(\"COALESCE(SUM(rh.amount_pt), 0)\", \"sum\")\n .where(\"rh.user_address = :user\", { user: getAddress(user) })\n .andWhere(\"rh.created_at_unix_sec >= :since\", { since: sinceUnixSec });\n\n if (pointTokenAddress !== undefined) {\n qb.andWhere(\"rh.token_address = :token\", {\n token: getAddress(pointTokenAddress),\n });\n } else {\n // When the caller didn't scope by token, sum across all tokens\n // for that user. Don't filter on token_address IS NULL — that\n // would silently miss entries that DID record a token.\n }\n\n const row = (await qb.getRawOne<{ sum: string | null }>()) ?? { sum: \"0\" };\n return BigInt(row.sum ?? \"0\");\n }\n\n async getLastRedeemedAtUnixSec(\n user: Address,\n pointTokenAddress?: Address,\n ): Promise<number | null> {\n const repo = this.dataSource.getRepository(RedemptionHistoryEntity);\n const qb = repo\n .createQueryBuilder(\"rh\")\n .select(\"rh.created_at_unix_sec\", \"ts\")\n .where(\"rh.user_address = :user\", { user: getAddress(user) })\n .orderBy(\"rh.created_at_unix_sec\", \"DESC\")\n .limit(1);\n\n if (pointTokenAddress !== undefined) {\n qb.andWhere(\"rh.token_address = :token\", {\n token: getAddress(pointTokenAddress),\n });\n }\n\n const row = await qb.getRawOne<{ ts: string | null }>();\n if (!row || row.ts === null) return null;\n return Number(row.ts);\n }\n\n async recordRedemption(entry: {\n user: Address;\n amountPt: bigint;\n pointTokenAddress?: Address;\n unixSec: number;\n reservationId?: string;\n }): Promise<void> {\n const repo = this.dataSource.getRepository(RedemptionHistoryEntity);\n const row = repo.create({\n userAddress: getAddress(entry.user),\n tokenAddress: entry.pointTokenAddress\n ? getAddress(entry.pointTokenAddress)\n : null,\n amountPt: entry.amountPt,\n createdAtUnixSec: String(entry.unixSec),\n reservationId: entry.reservationId ?? null,\n });\n await repo.save(row);\n }\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","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 type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Single consolidated initial schema for `@pafi-dev/issuer-postgres`.\n *\n * Combines what gg56 split into two migrations (`InitialSchema` +\n * `AddPendingCredits`) plus the `user_op_hash` column, since this is\n * the v1.4-and-after baseline. Issuers adopting the SDK from scratch\n * apply this once.\n *\n * Tables:\n * user_balances — off-chain point balance per (user, token)\n * locked_mint_requests — reservations during mint flow\n * pending_credits — reserved credits during burn/redeem flow\n * ledger_journal — append-only audit trail of every delta\n * indexer_cursors — PointIndexer / BurnIndexer block cursors\n *\n * Issuer-specific extensions (campaign rules, KYC tables, custom\n * scenarios) belong in a follow-up migration — never edit this file\n * in place once it ships.\n */\nexport class InitialSchema1700000000000 implements MigrationInterface {\n name = \"InitialSchema1700000000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS \"pgcrypto\"`);\n\n // ─── user_balances ──────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"user_balances\" (\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"balance\" numeric(78, 0) NOT NULL DEFAULT 0,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_user_balances\" PRIMARY KEY (\"user_address\", \"token_address\")\n )\n `);\n\n // ─── locked_mint_requests ───────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"locked_mint_requests\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n CONSTRAINT \"PK_locked_mint_requests\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_status\"\n ON \"locked_mint_requests\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_op_hash\"\n ON \"locked_mint_requests\" (\"user_op_hash\")\n `);\n\n // ─── pending_credits ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"pending_credits\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"resolved_at\" TIMESTAMP WITH TIME ZONE,\n CONSTRAINT \"PK_pending_credits\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_status\"\n ON \"pending_credits\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_tx_hash\"\n ON \"pending_credits\" (\"tx_hash\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_op_hash\"\n ON \"pending_credits\" (\"user_op_hash\")\n `);\n\n // ─── ledger_journal ─────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"ledger_journal\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"delta\" numeric(78, 0) NOT NULL,\n \"reason\" varchar(128) NOT NULL,\n \"tx_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_ledger_journal\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_ledger_journal_user_created\"\n ON \"ledger_journal\" (\"user_address\", \"token_address\", \"created_at\")\n `);\n\n // ─── indexer_cursors ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"indexer_cursors\" (\n \"id\" varchar(64) NOT NULL,\n \"next_block\" numeric(78, 0) NOT NULL,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_indexer_cursors\" PRIMARY KEY (\"id\")\n )\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`DROP TABLE \"indexer_cursors\"`);\n await queryRunner.query(`DROP INDEX \"IDX_ledger_journal_user_created\"`);\n await queryRunner.query(`DROP TABLE \"ledger_journal\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_tx_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_status\"`);\n await queryRunner.query(`DROP TABLE \"pending_credits\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_status\"`);\n await queryRunner.query(`DROP TABLE \"locked_mint_requests\"`);\n await queryRunner.query(`DROP TABLE \"user_balances\"`);\n }\n}\n","import { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Adds the `redemption_history` table — append-only log of successful\n * redemption initiates, indexed by (user, time) for the daily-limit\n * SUM() query.\n *\n * This is INDEPENDENT from the main InitialSchema (different timestamp,\n * later than 1700000000000). Issuers who already deployed InitialSchema\n * apply this on top.\n */\nexport class CreateRedemptionHistory1746230400001 implements MigrationInterface {\n name = \"CreateRedemptionHistory1746230400001\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`\n CREATE TABLE IF NOT EXISTS redemption_history (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_address varchar(42) NOT NULL,\n token_address varchar(42),\n amount_pt numeric(78, 0) NOT NULL,\n created_at_unix_sec bigint NOT NULL,\n reservation_id varchar(64),\n row_created_at timestamptz NOT NULL DEFAULT NOW(),\n CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)\n )\n `);\n\n await queryRunner.query(`\n CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created\n ON redemption_history (user_address, created_at_unix_sec DESC)\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS idx_redemption_history_user_created`,\n );\n await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);\n }\n}\n","export { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nexport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\n\nimport { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nimport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\n\n/**\n * All shipped migrations in chronological order. Drop into TypeORM's\n * `migrations` config:\n *\n * import { PAFI_MIGRATIONS } from \"@pafi-dev/issuer-postgres/migrations\";\n *\n * new DataSource({\n * entities: [...PAFI_ENTITIES, ...yourCustomEntities],\n * migrations: [...PAFI_MIGRATIONS, ...yourCustomMigrations],\n * });\n */\nexport const PAFI_MIGRATIONS = [\n InitialSchema1700000000000,\n CreateRedemptionHistory1746230400001,\n] as const;\n"],"mappings":"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","names":["Column","CreateDateColumn","Entity","Index","PrimaryGeneratedColumn","PrimaryGeneratedColumn","Column","CreateDateColumn","Entity","Index","Column","Entity","Column","Entity","Column","CreateDateColumn","Entity","Index","PrimaryGeneratedColumn","PrimaryGeneratedColumn","Column","CreateDateColumn","Entity","Index","Column","Entity","PrimaryColumn","UpdateDateColumn","PrimaryColumn","Column","UpdateDateColumn","Entity","getAddress","Column","CreateDateColumn","Entity","Index","PrimaryGeneratedColumn","PrimaryGeneratedColumn","Column","CreateDateColumn","Entity","Index","getAddress"]}
1
+ {"version":3,"sources":["../src/postgresPointLedger.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/postgresCursorStore.ts","../src/postgresRedemptionHistoryStore.ts","../src/entities/redemption-history.entity.ts","../src/entities/index.ts","../src/migrations/1700000000000-InitialSchema.ts","../src/migrations/1746230400001-CreateRedemptionHistory.ts","../src/migrations/1747500000000-AddJournalIdempotencyIndex.ts","../src/migrations/1747600000000-AddLockedMintCompositeIndexes.ts","../src/migrations/1747700000000-FixIdempotencyAddTokenAddress.ts","../src/migrations/index.ts"],"sourcesContent":["import type { DataSource, EntityManager } from \"typeorm\";\nimport { getAddress, type Address, type Hex } from \"viem\";\nimport type {\n IPointLedger,\n LockedMintRequest,\n MintingStatus,\n PendingCredit,\n} from \"@pafi-dev/issuer\";\n\nimport { LockedMintEntity } from \"./entities/locked-mint.entity\";\nimport { PendingCreditEntity } from \"./entities/pending-credit.entity\";\nimport { UserBalanceEntity } from \"./entities/user-balance.entity\";\nimport { LedgerJournalEntity } from \"./entities/ledger-journal.entity\";\n\n/**\n * Postgres SQLSTATE codes that indicate a transient transaction conflict\n * which is safe to retry. `40P01` = deadlock detected;\n * `40001` = serialization failure (only seen at SERIALIZABLE isolation\n * but harmless to retry at READ COMMITTED too).\n */\nconst RETRIABLE_PG_CODES = new Set([\"40P01\", \"40001\"]);\n\n/** Postgres SQLSTATE for unique_violation. */\nconst UNIQUE_VIOLATION = \"23505\";\n\n/** Constraint name of the partial unique index on `ledger_journal`. */\nconst JOURNAL_IDEMPOTENCY_CONSTRAINT =\n \"UQ_ledger_journal_user_token_tx_reason\";\n/**\n * Legacy constraint name from before audit PACI5-7. Retained for the\n * idempotency-violation classifier so that issuers running an older\n * schema (migration `1747500000000` applied, but\n * `1747700000000` not yet applied) still treat unique-violation\n * errors as benign no-ops instead of surfacing them as 500s.\n */\nconst LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT =\n \"UQ_ledger_journal_user_tx_reason\";\n\ninterface PgError {\n code?: string;\n message?: string;\n constraint?: string;\n driverError?: PgError;\n}\n\nfunction isRetriablePgError(err: unknown): boolean {\n const e = err as PgError | undefined;\n if (!e) return false;\n if (e.code && RETRIABLE_PG_CODES.has(e.code)) return true;\n // Some drivers attach the code on a nested cause / driverError. Defensive:\n // string-match the message as a last resort.\n if (e.message && /deadlock detected|could not serialize/i.test(e.message)) {\n return true;\n }\n return false;\n}\n\n/**\n * True iff the error is a Postgres unique-constraint violation caused by\n * the journal idempotency index. Used by `deductBalance` and\n * `resolveCreditByBurnTx` to detect a concurrent transaction that won\n * the race for the same `(user, txHash, reason)` tuple. The losing\n * caller treats this as a benign no-op — the work has already been\n * applied by the winner.\n */\nfunction isJournalIdempotencyViolation(err: unknown): boolean {\n const e = err as PgError | undefined;\n if (!e) return false;\n const code = e.code ?? e.driverError?.code;\n if (code !== UNIQUE_VIOLATION) return false;\n const constraint = e.constraint ?? e.driverError?.constraint;\n // If the driver surfaced the constraint name, trust it. Otherwise fall\n // back to a message match — pg's default error text contains the\n // constraint name, so the message check is reliable.\n if (\n constraint === JOURNAL_IDEMPOTENCY_CONSTRAINT ||\n constraint === LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT\n ) {\n return true;\n }\n const message = e.message ?? e.driverError?.message ?? \"\";\n return (\n message.includes(JOURNAL_IDEMPOTENCY_CONSTRAINT) ||\n message.includes(LEGACY_JOURNAL_IDEMPOTENCY_CONSTRAINT)\n );\n}\n\n/**\n * Wrap a transaction body in a deadlock-retry loop. Postgres can raise\n * `40P01` when concurrent transactions take row locks in incompatible\n * orders — once `lockForMinting`/`deductBalance` use `FOR UPDATE`,\n * deadlocks become a normal occurrence under contention. Without retry\n * the issuer surfaces them as 500s instead of absorbing them transparently.\n */\nasync function withDeadlockRetry<T>(\n dataSource: DataSource,\n fn: (tx: EntityManager) => Promise<T>,\n maxAttempts = 3,\n): Promise<T> {\n let attempt = 0;\n let delayMs = 25;\n for (;;) {\n attempt++;\n try {\n return await dataSource.transaction(fn);\n } catch (err) {\n if (attempt >= maxAttempts || !isRetriablePgError(err)) {\n throw err;\n }\n // Exponential backoff with jitter. 25ms → 50ms → 100ms typical.\n await new Promise((r) =>\n setTimeout(r, delayMs + Math.floor(Math.random() * delayMs)),\n );\n delayMs *= 2;\n }\n }\n}\n\nexport interface PostgresPointLedgerOptions {\n /**\n * Optional logger. When omitted, the service is silent. Pass a Nest\n * `Logger` / pino instance / `console` to surface debug + info\n * lines.\n */\n logger?: {\n debug?: (msg: string) => void;\n log?: (msg: string) => void;\n warn?: (msg: string) => void;\n };\n}\n\n/**\n * Postgres-backed `IPointLedger` — the reference impl every issuer\n * starts from. Framework-agnostic: takes a TypeORM `DataSource` in the\n * constructor, no NestJS decorators or DI tokens. Wrap in your\n * favorite injector.\n *\n * Implements every required + optional method from `IPointLedger`,\n * including the burn-side reverse flow (`reservePendingCredit`,\n * `resolveCreditByBurnTx`) and the bundler-receipt fallback hooks\n * (`bindMintUserOpHash`, `bindCreditUserOpHash`, `getMintLock`,\n * `getPendingCredit`).\n *\n * Multi-token: every method requires `tokenAddress` — there is no\n * \"default token\" bucket. Single-token issuers pass the same address\n * everywhere.\n */\nexport class PostgresPointLedger implements IPointLedger {\n private readonly logger: PostgresPointLedgerOptions[\"logger\"];\n\n constructor(\n private readonly dataSource: DataSource,\n options: PostgresPointLedgerOptions = {},\n ) {\n this.logger = options.logger;\n }\n\n // ---------------------------------------------------------------------\n // Read\n // ---------------------------------------------------------------------\n\n async getBalance(\n userAddress: Address,\n tokenAddress?: Address,\n ): Promise<bigint> {\n const { user, token } = normalize(userAddress, tokenAddress);\n\n // Pure read — `sumPendingLocks` filters `expires_at > now`, so\n // expired locks are naturally excluded without writing a status\n // update. The lock table grows monotonically until a periodic\n // background sweep runs `markExpiredLocks()` (see below). The\n // read query stays cheap because it filters by the composite\n // index `IDX_locked_mint_user_token_status_expires` — see\n // `LockedMintEntity` for the full index policy.\n const [balanceRow, locked] = await Promise.all([\n this.dataSource\n .getRepository(UserBalanceEntity)\n .findOne({ where: { userAddress: user, tokenAddress: token } }),\n this.sumPendingLocks(user, token),\n ]);\n const total = balanceRow?.balance ?? 0n;\n const available = total - locked;\n return available < 0n ? 0n : available;\n }\n\n /**\n * Background sweep — marks all expired PENDING locks as EXPIRED in\n * a single UPDATE. Issuers SHOULD call this periodically (e.g.\n * every 1-5 minutes via a cron / NestJS `@Interval`) to keep the\n * lock table from growing unbounded.\n *\n * A single sweep amortizes the write cost vs `getBalance` doing a\n * wide UPDATE on every read. Returns the number of rows transitioned.\n *\n * @example\n * ```ts\n * import { Interval } from \"@nestjs/schedule\";\n *\n * @Injectable()\n * export class LockSweepService {\n * constructor(private readonly ledger: PostgresPointLedger) {}\n *\n * @Interval(5 * 60 * 1000) // 5 minutes\n * async sweep() {\n * const swept = await this.ledger.markExpiredLocks();\n * this.logger.debug(`expired ${swept} mint locks`);\n * }\n * }\n * ```\n */\n async markExpiredLocks(): Promise<number> {\n const result = await this.dataSource\n .getRepository(LockedMintEntity)\n .createQueryBuilder()\n .update()\n .set({ status: \"EXPIRED\" })\n .where(\"status = :pending\", { pending: \"PENDING\" })\n .andWhere(\"expires_at <= :now\", { now: new Date() })\n .execute();\n const swept = result.affected ?? 0;\n if (swept > 0) {\n this.logger?.debug?.(`markExpiredLocks: swept ${swept} mint locks`);\n }\n return swept;\n }\n\n async getLockedRequests(\n userAddress: Address,\n tokenAddress?: Address,\n ): Promise<LockedMintRequest[]> {\n const { user, token } = normalize(userAddress, tokenAddress);\n const rows = await this.dataSource\n .getRepository(LockedMintEntity)\n .find({\n where: { userAddress: user, tokenAddress: token, status: \"PENDING\" },\n order: { createdAt: \"ASC\" },\n });\n return rows.map((row) => this.toSdkLock(row));\n }\n\n async getMintLock(\n lockId: string,\n userAddress?: Address,\n ): Promise<LockedMintRequest | null> {\n const row = await this.dataSource\n .getRepository(LockedMintEntity)\n .findOne({ where: { id: lockId } });\n if (!row) return null;\n if (\n userAddress &&\n row.userAddress.toLowerCase() !== userAddress.toLowerCase()\n ) {\n return null;\n }\n return this.toSdkLock(row);\n }\n\n /** Raw TypeORM row — escape hatch for callers that need entity fields. */\n async getMintLockEntity(lockId: string): Promise<LockedMintEntity | null> {\n return this.dataSource\n .getRepository(LockedMintEntity)\n .findOne({ where: { id: lockId } });\n }\n\n async getPendingCredit(\n lockId: string,\n userAddress?: Address,\n ): Promise<PendingCredit | null> {\n const row = await this.dataSource\n .getRepository(PendingCreditEntity)\n .findOne({ where: { id: lockId } });\n if (!row) return null;\n if (\n userAddress &&\n row.userAddress.toLowerCase() !== userAddress.toLowerCase()\n ) {\n return null;\n }\n return {\n lockId: row.id,\n userAddress: getAddress(row.userAddress) as Address,\n amount: row.amount,\n tokenAddress: row.tokenAddress\n ? (getAddress(row.tokenAddress) as Address)\n : undefined,\n status: row.status as PendingCredit[\"status\"],\n createdAt: row.createdAt.getTime(),\n expiresAt: row.expiresAt.getTime(),\n txHash: (row.txHash as Hex | null) ?? undefined,\n resolvedAt: row.resolvedAt?.getTime(),\n userOpHash: (row.userOpHash as Hex | null) ?? undefined,\n };\n }\n\n /** Raw TypeORM row escape hatch for credits. */\n async getPendingCreditEntity(\n lockId: string,\n ): Promise<PendingCreditEntity | null> {\n return this.dataSource\n .getRepository(PendingCreditEntity)\n .findOne({ where: { id: lockId } });\n }\n\n /**\n * Paginated list of a user's mint requests across all statuses and\n * all tokens — used by `GET /user/transactions` reference endpoint.\n */\n async listUserTransactions(\n userAddress: Address,\n limit: number,\n offset: number,\n ): Promise<{ rows: LockedMintEntity[]; total: number }> {\n const user = getAddress(userAddress);\n const [rows, total] = await this.dataSource\n .getRepository(LockedMintEntity)\n .findAndCount({\n where: { userAddress: user },\n order: { createdAt: \"DESC\" },\n take: limit,\n skip: offset,\n });\n return { rows, total };\n }\n\n // ---------------------------------------------------------------------\n // Write\n // ---------------------------------------------------------------------\n\n async creditBalance(\n userAddress: Address,\n amount: bigint,\n reason: string,\n tokenAddress?: Address,\n ): Promise<void> {\n if (amount <= 0n) {\n throw new Error(\"creditBalance: amount must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n await withDeadlockRetry(this.dataSource, async (tx) => {\n // FOR UPDATE on the existing balance row — avoids lost-update under\n // concurrent credits (each tx reads stale total + writes incremented\n // value at READ COMMITTED, dropping increments). UPSERT cannot lock\n // a row that doesn't exist yet, so first-credit case relies on the\n // unique constraint (user_address, token_address) to serialize.\n const existing = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n const next = (existing?.balance ?? 0n) + amount;\n\n await tx\n .getRepository(UserBalanceEntity)\n .upsert(\n { userAddress: user, tokenAddress: token, balance: next },\n { conflictPaths: [\"userAddress\", \"tokenAddress\"] },\n );\n\n await tx.getRepository(LedgerJournalEntity).insert({\n userAddress: user,\n tokenAddress: token,\n delta: amount,\n reason,\n });\n });\n\n this.logger?.debug?.(`credit ${user}[${token}] +${amount} (${reason})`);\n }\n\n async lockForMinting(\n userAddress: Address,\n amount: bigint,\n lockDurationMs: number,\n tokenAddress?: Address,\n ): Promise<string> {\n if (amount <= 0n) {\n throw new Error(\"lockForMinting: amount must be positive\");\n }\n if (lockDurationMs <= 0) {\n throw new Error(\"lockForMinting: lockDurationMs must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n return withDeadlockRetry(this.dataSource, async (tx) => {\n // SELECT … FOR UPDATE on the user's balance row to prevent TOCTOU.\n // Without this, two concurrent lockForMinting() at READ COMMITTED can\n // both see the same `available` and both insert lock rows, allowing\n // the issuer to over-commit ledger balance against on-chain mints.\n const balanceRow = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n const total = balanceRow?.balance ?? 0n;\n\n // Sum PENDING locks while holding the balance row lock — any\n // concurrent insert would serialize behind us on the same row.\n const pendingTotal = await tx\n .getRepository(LockedMintEntity)\n .createQueryBuilder(\"lock\")\n .select(\"COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)\", \"sum\")\n .where(\"lock.user_address = :user\", { user })\n .andWhere(\"lock.token_address = :token\", { token })\n .andWhere(\"lock.status = :pending\", { pending: \"PENDING\" })\n .andWhere(\"lock.expires_at > :now\", { now: new Date() })\n .getRawOne<{ sum: string }>();\n\n const locked = pendingTotal ? BigInt(pendingTotal.sum) : 0n;\n const available = total - locked;\n if (available < amount) {\n throw new Error(\n `Insufficient balance: available=${available}, requested=${amount}`,\n );\n }\n\n const lock = await tx.getRepository(LockedMintEntity).save({\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n expiresAt: new Date(Date.now() + lockDurationMs),\n });\n\n this.logger?.debug?.(\n `lock ${lock.id} ${user}[${token}] amount=${amount}`,\n );\n return lock.id;\n });\n }\n\n async releaseLock(lockId: string): Promise<void> {\n const result = await this.dataSource\n .getRepository(LockedMintEntity)\n .delete({ id: lockId, status: \"PENDING\" });\n\n if ((result.affected ?? 0) > 0) {\n this.logger?.debug?.(`release lock ${lockId}`);\n }\n }\n\n async deductBalance(\n userAddress: Address,\n amount: bigint,\n txHash: Hex,\n tokenAddress?: Address,\n ): Promise<void> {\n if (amount <= 0n) {\n throw new Error(\"deductBalance: amount must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n // Idempotency on txHash — the `IPointLedger.deductBalance` contract\n // promises that calling twice with the same `txHash` is a no-op.\n // Two layers of defense:\n // (1) In-transaction SELECT below — fast path; serializes with any\n // concurrent deduct via the balance row's FOR UPDATE lock so\n // the journal read happens after the racing tx commits.\n // (2) Partial UNIQUE index on `ledger_journal (user_address,\n // tx_hash, reason) WHERE tx_hash IS NOT NULL` — last-line\n // defense for any race the SELECT couldn't see (e.g. two\n // isolated indexer pods that race past the balance lock\n // acquisition order). A unique-violation rolls the tx back\n // and surfaces here as `isJournalIdempotencyViolation`, which\n // we treat as a benign no-op (the work was applied by the\n // winner).\n try {\n await withDeadlockRetry(this.dataSource, async (tx) => {\n // FOR UPDATE on the balance row serializes us with any\n // concurrent deductBalance for the same (user, token), so the\n // idempotency SELECT below sees a stable, committed view of\n // the journal.\n const balance = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n\n // Idempotency check — must include `tokenAddress` (audit PACI5-7).\n // A single transaction that mints two different PointTokens for\n // the same user (batched EIP-7702 claim, or Pimlico bundler\n // grouping two single-mint UserOps into one bundle) emits two\n // Transfer events with the same txHash but different token\n // contracts. Without `tokenAddress` in the lookup the second\n // indexer's debit would be silently shadowed by the first\n // token's journal row, while `PointIndexer.finalize` still\n // flipped the second lock to MINTED — silent supply-invariant\n // violation. The partial unique index\n // `UQ_ledger_journal_user_token_tx_reason` is the schema-level\n // backstop for this check.\n const already = await tx\n .getRepository(LedgerJournalEntity)\n .findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n txHash,\n reason: \"MINT_CONFIRMED\",\n },\n });\n if (already) {\n this.logger?.debug?.(\n `deductBalance: idempotent skip tx=${txHash} user=${user} token=${token}`,\n );\n return;\n }\n\n if (!balance || balance.balance < amount) {\n throw new Error(\n `Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`,\n );\n }\n\n await tx.getRepository(UserBalanceEntity).update(\n { userAddress: user, tokenAddress: token },\n { balance: balance.balance - amount },\n );\n\n await tx.getRepository(LedgerJournalEntity).insert({\n userAddress: user,\n tokenAddress: token,\n delta: -amount,\n reason: \"MINT_CONFIRMED\",\n txHash,\n });\n\n // Resolve the oldest matching PENDING lock atomically.\n const match = await tx.getRepository(LockedMintEntity).findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n },\n order: { createdAt: \"ASC\" },\n });\n if (match) {\n await tx\n .getRepository(LockedMintEntity)\n .update({ id: match.id }, { status: \"MINTED\", txHash });\n }\n });\n } catch (err) {\n if (isJournalIdempotencyViolation(err)) {\n // A concurrent tx committed the same (user, txHash,\n // MINT_CONFIRMED) row between our SELECT and our INSERT. The\n // partial unique index rejected our INSERT; the whole tx\n // rolled back, so our balance update is undone. Idempotent\n // no-op — the winning tx already deducted balance + resolved\n // the lock.\n this.logger?.debug?.(\n `deductBalance: idempotent (concurrent race) tx=${txHash} user=${user}`,\n );\n return;\n }\n throw err;\n }\n\n this.logger?.log?.(`deduct ${user}[${token}] -${amount} tx=${txHash}`);\n }\n\n async updateMintStatus(\n lockId: string,\n status: MintingStatus,\n txHash?: Hex,\n ): Promise<void> {\n // Terminal-state guard — only PENDING locks transition. See the\n // `IPointLedger.updateMintStatus` docstring for the contract. Both\n // production callers (`PointIndexer.finalize` and the bundler-\n // receipt fallback in `statusHandlers`) are best-effort and treat\n // a no-op as success; we surface the no-op at debug level for\n // observability but never throw.\n const update: Partial<LockedMintEntity> = { status };\n if (txHash) update.txHash = txHash;\n\n const result = await this.dataSource\n .getRepository(LockedMintEntity)\n .createQueryBuilder()\n .update()\n .set(update)\n .where(\"id = :id\", { id: lockId })\n .andWhere(\"status = :pending\", { pending: \"PENDING\" })\n .execute();\n\n if ((result.affected ?? 0) === 0) {\n this.logger?.debug?.(\n `updateMintStatus: lock ${lockId} not in PENDING (terminal state), no-op`,\n );\n }\n }\n\n async bindMintUserOpHash(lockId: string, userOpHash: Hex): Promise<void> {\n await this.dataSource\n .getRepository(LockedMintEntity)\n .update({ id: lockId }, { userOpHash });\n }\n\n async bindCreditUserOpHash(lockId: string, userOpHash: Hex): Promise<void> {\n await this.dataSource\n .getRepository(PendingCreditEntity)\n .update({ id: lockId }, { userOpHash });\n }\n\n // ---------------------------------------------------------------------\n // Reverse flow (burn → off-chain credit)\n // ---------------------------------------------------------------------\n\n async reservePendingCredit(\n userAddress: Address,\n amount: bigint,\n durationMs: number,\n tokenAddress?: Address,\n ): Promise<string> {\n if (amount <= 0n) {\n throw new Error(\"reservePendingCredit: amount must be positive\");\n }\n if (durationMs <= 0) {\n throw new Error(\"reservePendingCredit: durationMs must be positive\");\n }\n const { user, token } = normalize(userAddress, tokenAddress);\n\n const row = await this.dataSource\n .getRepository(PendingCreditEntity)\n .save({\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n expiresAt: new Date(Date.now() + durationMs),\n });\n\n this.logger?.debug?.(\n `reserve pending credit ${row.id} ${user}[${token}] +${amount}`,\n );\n return row.id;\n }\n\n async resolveCreditByBurnTx(lockId: string, txHash: Hex): Promise<void> {\n // Symmetric idempotency to `deductBalance`: the partial UNIQUE\n // index on `ledger_journal` rejects a second\n // (user, txHash, BURN_FOR_CREDIT) row. On violation we re-run once\n // — the retry's sibling-defense (`alreadyResolved` lookup below)\n // now sees the winning tx's RESOLVED row and marks this credit\n // RESOLVED without re-applying balance.\n const run = async () =>\n withDeadlockRetry(this.dataSource, async (tx) => {\n // FOR UPDATE on the credit row — burn-side mirror of mint resolution.\n // Concurrent calls (chain reorg / duplicate event) at READ COMMITTED\n // could both pass the status check and double-credit balance.\n const credit = await tx\n .getRepository(PendingCreditEntity)\n .createQueryBuilder(\"credit\")\n .setLock(\"pessimistic_write\")\n .where(\"credit.id = :id\", { id: lockId })\n .getOne();\n\n if (!credit) {\n throw new Error(\n `resolveCreditByBurnTx: unknown pending credit ${lockId}`,\n );\n }\n\n if (credit.status === \"RESOLVED\") {\n if (credit.txHash === txHash) return; // idempotent replay\n throw new Error(\n `resolveCreditByBurnTx: credit ${lockId} already resolved with a different txHash`,\n );\n }\n\n if (credit.status === \"EXPIRED\") {\n throw new Error(\n `resolveCreditByBurnTx: credit ${lockId} already expired — burn landed too late`,\n );\n }\n\n const user = credit.userAddress as Address;\n const token = credit.tokenAddress as Address;\n\n // Defense-in-depth — same `txHash` already credited a sibling\n // credit for the same (user, token). Mark this credit resolved\n // without re-applying balance.\n const alreadyResolved = await tx\n .getRepository(PendingCreditEntity)\n .findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n txHash,\n status: \"RESOLVED\",\n },\n });\n if (alreadyResolved) {\n await tx\n .getRepository(PendingCreditEntity)\n .update(\n { id: lockId },\n { status: \"RESOLVED\", txHash, resolvedAt: new Date() },\n );\n return;\n }\n\n // FOR UPDATE on balance row prevents lost-update with concurrent\n // creditBalance / resolveCreditByBurnTx (both increment).\n const balance = await tx\n .getRepository(UserBalanceEntity)\n .createQueryBuilder(\"balance\")\n .setLock(\"pessimistic_write\")\n .where(\"balance.user_address = :user\", { user })\n .andWhere(\"balance.token_address = :token\", { token })\n .getOne();\n const next = (balance?.balance ?? 0n) + credit.amount;\n\n await tx\n .getRepository(UserBalanceEntity)\n .upsert(\n { userAddress: user, tokenAddress: token, balance: next },\n { conflictPaths: [\"userAddress\", \"tokenAddress\"] },\n );\n\n await tx.getRepository(LedgerJournalEntity).insert({\n userAddress: user,\n tokenAddress: token,\n delta: credit.amount,\n reason: \"BURN_FOR_CREDIT\",\n txHash,\n });\n\n await tx\n .getRepository(PendingCreditEntity)\n .update(\n { id: lockId },\n { status: \"RESOLVED\", txHash, resolvedAt: new Date() },\n );\n });\n\n try {\n await run();\n } catch (err) {\n if (isJournalIdempotencyViolation(err)) {\n this.logger?.debug?.(\n `resolveCreditByBurnTx: concurrent race on tx=${txHash} lock=${lockId}, replaying via sibling defense`,\n );\n // One-shot retry. The original tx rolled back, so the credit\n // row is still PENDING; the retry's `alreadyResolved` lookup\n // now sees the winner's RESOLVED row and takes the sibling-\n // defense path (mark this credit RESOLVED, no balance change).\n await run();\n } else {\n throw err;\n }\n }\n\n this.logger?.log?.(`resolve pending credit ${lockId} tx=${txHash}`);\n }\n\n /**\n * Used by `BurnIndexer.matchLockId` to resolve an on-chain burn\n * event back to a pending credit row. Returns the oldest matching\n * `(user, token, amount, status: PENDING)` lockId, or undefined\n * when no match exists (unsolicited burn — indexer skips).\n */\n async findPendingCreditLockId(\n userAddress: Address,\n amount: bigint,\n tokenAddress: Address,\n ): Promise<string | undefined> {\n const { user, token } = normalize(userAddress, tokenAddress);\n const row = await this.dataSource\n .getRepository(PendingCreditEntity)\n .findOne({\n where: {\n userAddress: user,\n tokenAddress: token,\n amount,\n status: \"PENDING\",\n },\n order: { createdAt: \"ASC\" },\n });\n return row?.id;\n }\n\n // ---------------------------------------------------------------------\n // Internals\n // ---------------------------------------------------------------------\n\n private async sumPendingLocks(\n userAddress: Address,\n tokenAddress: Address,\n ): Promise<bigint> {\n const row = await this.dataSource\n .getRepository(LockedMintEntity)\n .createQueryBuilder(\"lock\")\n .select(\"COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)\", \"sum\")\n .where(\"lock.user_address = :user\", { user: userAddress })\n .andWhere(\"lock.token_address = :token\", { token: tokenAddress })\n .andWhere(\"lock.status = :pending\", { pending: \"PENDING\" })\n .andWhere(\"lock.expires_at > :now\", { now: new Date() })\n .getRawOne<{ sum: string }>();\n return row ? BigInt(row.sum) : 0n;\n }\n\n private toSdkLock(row: LockedMintEntity): LockedMintRequest {\n const out: LockedMintRequest = {\n lockId: row.id,\n userAddress: row.userAddress as Address,\n tokenAddress: row.tokenAddress as Address,\n amount: row.amount,\n status: row.status,\n createdAt: row.createdAt.getTime(),\n expiresAt: row.expiresAt.getTime(),\n };\n if (row.txHash) out.txHash = row.txHash as Hex;\n if (row.userOpHash) out.userOpHash = row.userOpHash as Hex;\n return out;\n }\n}\n\n/**\n * Multi-token guard — throw if `tokenAddress` is missing on any\n * mutating call. Single-token issuers must still pass their token\n * address explicitly so reads + writes never fall into a \"default\"\n * bucket the application never queries.\n */\nfunction normalize(\n userAddress: Address,\n tokenAddress: Address | undefined,\n): { user: Address; token: Address } {\n if (!tokenAddress) {\n throw new Error(\n \"PostgresPointLedger: tokenAddress is required on every call (multi-token ledger)\",\n );\n }\n return {\n user: getAddress(userAddress),\n token: getAddress(tokenAddress),\n };\n}\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 type { DataSource } from \"typeorm\";\nimport type { IIndexerCursorStore } from \"@pafi-dev/issuer\";\n\nimport { IndexerCursorEntity } from \"./entities/indexer-cursor.entity\";\n\n/**\n * Postgres-backed indexer cursor store. Lets indexers survive\n * restarts — on boot, the indexer reads the last persisted block and\n * resumes scanning from there.\n *\n * Multiple indexers (e.g. `PointIndexer` for Mint events +\n * `BurnIndexer` per token for Transfer→0x0) share the same table via\n * different `cursorId`s. Construct with `cursorId = \"default\"` for\n * the primary mint indexer, or call `forKey(id)` to derive a sibling\n * store for a secondary indexer.\n */\nexport class PostgresCursorStore implements IIndexerCursorStore {\n constructor(\n private readonly dataSource: DataSource,\n private readonly cursorId: string = \"default\",\n ) {}\n\n async load(): Promise<bigint | undefined> {\n const row = await this.dataSource\n .getRepository(IndexerCursorEntity)\n .findOne({ where: { id: this.cursorId } });\n return row?.nextBlock;\n }\n\n async save(blockNumber: bigint): Promise<void> {\n await this.dataSource\n .getRepository(IndexerCursorEntity)\n .upsert(\n { id: this.cursorId, nextBlock: blockNumber },\n { conflictPaths: [\"id\"] },\n );\n }\n\n /** Derived store keyed by a different `id` — for sibling indexers. */\n forKey(cursorId: string): IIndexerCursorStore {\n return new PostgresCursorStore(this.dataSource, cursorId);\n }\n}\n","import type { DataSource } from \"typeorm\";\nimport { getAddress, type Address } from \"viem\";\nimport type { IRedemptionHistoryStore } from \"@pafi-dev/issuer\";\n\nimport { RedemptionHistoryEntity } from \"./entities/redemption-history.entity\";\n\n/**\n * Postgres-backed IRedemptionHistoryStore. Append-only — every\n * `recordRedemption` writes a new row. Reads (`sumRedeemedSince` /\n * `getLastRedeemedAtUnixSec`) hit the (user, time) composite index.\n *\n * Addresses are normalized to checksum form on write and lower-cased\n * on read so the index works regardless of casing inconsistency. The\n * entity stores the canonical (checksummed) form.\n */\nexport class PostgresRedemptionHistoryStore implements IRedemptionHistoryStore {\n constructor(private readonly dataSource: DataSource) {}\n\n async sumRedeemedSince(\n user: Address,\n sinceUnixSec: number,\n pointTokenAddress?: Address,\n ): Promise<bigint> {\n const repo = this.dataSource.getRepository(RedemptionHistoryEntity);\n const qb = repo\n .createQueryBuilder(\"rh\")\n .select(\"COALESCE(SUM(rh.amount_pt), 0)\", \"sum\")\n .where(\"rh.user_address = :user\", { user: getAddress(user) })\n .andWhere(\"rh.created_at_unix_sec >= :since\", { since: sinceUnixSec });\n\n if (pointTokenAddress !== undefined) {\n qb.andWhere(\"rh.token_address = :token\", {\n token: getAddress(pointTokenAddress),\n });\n } else {\n // When the caller didn't scope by token, sum across all tokens\n // for that user. Don't filter on token_address IS NULL — that\n // would silently miss entries that DID record a token.\n }\n\n const row = (await qb.getRawOne<{ sum: string | null }>()) ?? { sum: \"0\" };\n return BigInt(row.sum ?? \"0\");\n }\n\n async getLastRedeemedAtUnixSec(\n user: Address,\n pointTokenAddress?: Address,\n ): Promise<number | null> {\n const repo = this.dataSource.getRepository(RedemptionHistoryEntity);\n const qb = repo\n .createQueryBuilder(\"rh\")\n .select(\"rh.created_at_unix_sec\", \"ts\")\n .where(\"rh.user_address = :user\", { user: getAddress(user) })\n .orderBy(\"rh.created_at_unix_sec\", \"DESC\")\n .limit(1);\n\n if (pointTokenAddress !== undefined) {\n qb.andWhere(\"rh.token_address = :token\", {\n token: getAddress(pointTokenAddress),\n });\n }\n\n const row = await qb.getRawOne<{ ts: string | null }>();\n if (!row || row.ts === null) return null;\n return Number(row.ts);\n }\n\n async recordRedemption(entry: {\n user: Address;\n amountPt: bigint;\n pointTokenAddress?: Address;\n unixSec: number;\n reservationId?: string;\n }): Promise<void> {\n const repo = this.dataSource.getRepository(RedemptionHistoryEntity);\n const row = repo.create({\n userAddress: getAddress(entry.user),\n tokenAddress: entry.pointTokenAddress\n ? getAddress(entry.pointTokenAddress)\n : null,\n amountPt: entry.amountPt,\n createdAtUnixSec: String(entry.unixSec),\n reservationId: entry.reservationId ?? null,\n });\n await repo.save(row);\n }\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","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 type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Single consolidated initial schema for `@pafi-dev/issuer-postgres`.\n *\n * Issuers adopting the SDK from scratch apply this migration once;\n * subsequent schema changes ship as follow-up migrations.\n *\n * Tables:\n * user_balances — off-chain point balance per (user, token)\n * locked_mint_requests — reservations during mint flow\n * pending_credits — reserved credits during burn/redeem flow\n * ledger_journal — append-only audit trail of every delta\n * indexer_cursors — PointIndexer / BurnIndexer block cursors\n *\n * Issuer-specific extensions (campaign rules, KYC tables, custom\n * scenarios) belong in a follow-up migration — never edit this file\n * in place once it ships.\n */\nexport class InitialSchema1700000000000 implements MigrationInterface {\n name = \"InitialSchema1700000000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS \"pgcrypto\"`);\n\n // ─── user_balances ──────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"user_balances\" (\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"balance\" numeric(78, 0) NOT NULL DEFAULT 0,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_user_balances\" PRIMARY KEY (\"user_address\", \"token_address\")\n )\n `);\n\n // ─── locked_mint_requests ───────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"locked_mint_requests\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n CONSTRAINT \"PK_locked_mint_requests\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_status\"\n ON \"locked_mint_requests\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_op_hash\"\n ON \"locked_mint_requests\" (\"user_op_hash\")\n `);\n\n // ─── pending_credits ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"pending_credits\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"resolved_at\" TIMESTAMP WITH TIME ZONE,\n CONSTRAINT \"PK_pending_credits\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_status\"\n ON \"pending_credits\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_tx_hash\"\n ON \"pending_credits\" (\"tx_hash\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_op_hash\"\n ON \"pending_credits\" (\"user_op_hash\")\n `);\n\n // ─── ledger_journal ─────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"ledger_journal\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"delta\" numeric(78, 0) NOT NULL,\n \"reason\" varchar(128) NOT NULL,\n \"tx_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_ledger_journal\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_ledger_journal_user_created\"\n ON \"ledger_journal\" (\"user_address\", \"token_address\", \"created_at\")\n `);\n\n // ─── indexer_cursors ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"indexer_cursors\" (\n \"id\" varchar(64) NOT NULL,\n \"next_block\" numeric(78, 0) NOT NULL,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_indexer_cursors\" PRIMARY KEY (\"id\")\n )\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`DROP TABLE \"indexer_cursors\"`);\n await queryRunner.query(`DROP INDEX \"IDX_ledger_journal_user_created\"`);\n await queryRunner.query(`DROP TABLE \"ledger_journal\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_tx_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_status\"`);\n await queryRunner.query(`DROP TABLE \"pending_credits\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_status\"`);\n await queryRunner.query(`DROP TABLE \"locked_mint_requests\"`);\n await queryRunner.query(`DROP TABLE \"user_balances\"`);\n }\n}\n","import { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Adds the `redemption_history` table — append-only log of successful\n * redemption initiates, indexed by (user, time) for the daily-limit\n * SUM() query.\n *\n * This is INDEPENDENT from the main InitialSchema (different timestamp,\n * later than 1700000000000). Issuers who already deployed InitialSchema\n * apply this on top.\n */\nexport class CreateRedemptionHistory1746230400001 implements MigrationInterface {\n name = \"CreateRedemptionHistory1746230400001\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`\n CREATE TABLE IF NOT EXISTS redemption_history (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_address varchar(42) NOT NULL,\n token_address varchar(42),\n amount_pt numeric(78, 0) NOT NULL,\n created_at_unix_sec bigint NOT NULL,\n reservation_id varchar(64),\n row_created_at timestamptz NOT NULL DEFAULT NOW(),\n CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)\n )\n `);\n\n await queryRunner.query(`\n CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created\n ON redemption_history (user_address, created_at_unix_sec DESC)\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS idx_redemption_history_user_created`,\n );\n await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);\n }\n}\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Adds a partial unique index on `ledger_journal` to enforce\n * idempotency for indexer-driven reasons:\n *\n * UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL\n *\n * Last-line defense against double-deduct / double-credit when the\n * mint or burn indexer replays the same on-chain event (reorg, pod\n * restart, duplicate replica). Rows with `tx_hash IS NULL` (off-chain\n * credits like AIRDROP, refunds) are unaffected by the partial\n * predicate.\n *\n * Pre-flight: existing duplicate `(user_address, tx_hash, reason)`\n * rows with non-null `tx_hash` will block index creation. Operators\n * who suspect prior double-processing should reconcile manually\n * before applying. The query below surfaces offenders:\n *\n * SELECT user_address, tx_hash, reason, COUNT(*)\n * FROM ledger_journal\n * WHERE tx_hash IS NOT NULL\n * GROUP BY user_address, tx_hash, reason\n * HAVING COUNT(*) > 1;\n */\nexport class AddJournalIdempotencyIndex1747500000000\n implements MigrationInterface\n{\n name = \"AddJournalIdempotencyIndex1747500000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_ledger_journal_user_tx_reason\"\n ON \"ledger_journal\" (\"user_address\", \"tx_hash\", \"reason\")\n WHERE \"tx_hash\" IS NOT NULL\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS \"UQ_ledger_journal_user_tx_reason\"`,\n );\n }\n}\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Replaces the narrow `IDX_locked_mint_user_status` (3 cols:\n * `user_address`, `token_address`, `status`) with three composite\n * indexes that match the actual query shapes used by the SDK.\n *\n * Index rationale:\n *\n * `IDX_locked_mint_user_token_status_expires`\n * Covers `sumPendingLocks` (called inside every `getBalance` +\n * `lockForMinting`). The 4-col index lets Postgres push\n * `expires_at > NOW()` into the index range scan instead of\n * post-filtering rows from the heap. Without it, unswept\n * expired locks cause a latency cliff under load.\n *\n * `IDX_locked_mint_user_token_amount_status`\n * Covers `PointIndexer.pickMatchingLock` and the lock-resolution\n * `findOne` inside `deductBalance`. The amount column is the\n * selectivity-critical predicate (PENDING locks for the same\n * user/token often share a token but differ by amount).\n *\n * `IDX_locked_mint_pending_expires` (partial)\n * Covers the `markExpiredLocks` sweep. Partial predicate\n * `WHERE status = 'PENDING'` keeps the index small — terminal\n * rows (`MINTED` / `EXPIRED` / `FAILED`) drop out automatically.\n *\n * --- Operational notes ---\n *\n * `CREATE INDEX CONCURRENTLY` runs without a write lock on the table\n * — safe on production traffic — but cannot run inside a transaction\n * block. `transaction = false` disables TypeORM's per-migration\n * transaction wrapping. Trade-off: if this migration partially\n * fails, one or more indexes may be left in an INVALID state\n * (`pg_index.indisvalid = false`). Recovery:\n *\n * -- find half-built indexes\n * SELECT i.relname AS index_name\n * FROM pg_index x\n * JOIN pg_class i ON i.oid = x.indexrelid\n * JOIN pg_class t ON t.oid = x.indrelid\n * WHERE t.relname = 'locked_mint_requests' AND NOT x.indisvalid;\n *\n * -- drop and rerun the migration\n * DROP INDEX CONCURRENTLY IF EXISTS \"<index_name>\";\n *\n * On large tables (10M+ rows) each `CREATE INDEX CONCURRENTLY` can\n * take several minutes — it makes two table passes plus a final\n * synchronisation. Plan accordingly.\n */\nexport class AddLockedMintCompositeIndexes1747600000000\n implements MigrationInterface\n{\n name = \"AddLockedMintCompositeIndexes1747600000000\";\n\n /**\n * CONCURRENTLY index DDL cannot run inside a transaction. Tell\n * TypeORM to issue these statements directly.\n */\n transaction = false as const;\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n // Order matters: create the replacement indexes BEFORE dropping\n // the narrow one, so concurrent reads always have at least one\n // usable index throughout the migration.\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS\n \"IDX_locked_mint_user_token_status_expires\"\n ON \"locked_mint_requests\"\n (\"user_address\", \"token_address\", \"status\", \"expires_at\")\n `);\n\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS\n \"IDX_locked_mint_user_token_amount_status\"\n ON \"locked_mint_requests\"\n (\"user_address\", \"token_address\", \"amount\", \"status\")\n `);\n\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS\n \"IDX_locked_mint_pending_expires\"\n ON \"locked_mint_requests\" (\"expires_at\")\n WHERE \"status\" = 'PENDING'\n `);\n\n // The old `(user_address, token_address, status)` index is a\n // strict prefix of the new 4-col index, so dropping it is safe.\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_user_status\"`,\n );\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n // Restore the original narrow index first, then drop the new\n // ones — same \"always have a usable index\" invariant in reverse.\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS \"IDX_locked_mint_user_status\"\n ON \"locked_mint_requests\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_pending_expires\"`,\n );\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_user_token_amount_status\"`,\n );\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_user_token_status_expires\"`,\n );\n }\n}\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Audit PACI5-7 — Tx-hash idempotency tuple collapses multi-token mints\n * into a single debit.\n *\n * The original idempotency index (`UQ_ledger_journal_user_tx_reason`,\n * shipped in `1747500000000-AddJournalIdempotencyIndex`) was\n *\n * UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL\n *\n * which omits `token_address`. When a single transaction mints two\n * different PointTokens for the same user (legitimate batched\n * EIP-7702 claim, or Pimlico bundler grouping two single-mint\n * UserOps into one bundle), the second indexer's \"already processed?\"\n * lookup matches the first token's journal row and the debit is\n * silently skipped — yet `PointIndexer.finalize` still flips the\n * second lock to MINTED. Result: on-chain mint with no off-chain\n * debit → off-chain balance stays spendable → user can re-mint until\n * the issuer cap is exhausted.\n *\n * This migration replaces the tuple with the corrected one:\n *\n * UNIQUE (user_address, token_address, tx_hash, reason)\n * WHERE tx_hash IS NOT NULL\n *\n * The `deductBalance` \"already\" lookup is updated to include\n * `token_address` in the same release.\n *\n * Pre-flight: the new index is strictly more permissive than the old\n * one (adds a column to the key, never removes), so no existing row\n * pair that was unique under the old index can collide under the new\n * one. Safe to apply on populated databases without reconciliation.\n *\n * Down migration restores the original (vulnerable) index — intended\n * only for emergency rollback. Operators rolling back must also\n * revert the `deductBalance` lookup in `postgresPointLedger.ts`;\n * otherwise the application would query a four-column tuple against\n * a three-column index and idempotency would fail open.\n */\nexport class FixIdempotencyAddTokenAddress1747700000000\n implements MigrationInterface\n{\n name = \"FixIdempotencyAddTokenAddress1747700000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS \"UQ_ledger_journal_user_tx_reason\"`,\n );\n await queryRunner.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_ledger_journal_user_token_tx_reason\"\n ON \"ledger_journal\" (\"user_address\", \"token_address\", \"tx_hash\", \"reason\")\n WHERE \"tx_hash\" IS NOT NULL\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS \"UQ_ledger_journal_user_token_tx_reason\"`,\n );\n await queryRunner.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_ledger_journal_user_tx_reason\"\n ON \"ledger_journal\" (\"user_address\", \"tx_hash\", \"reason\")\n WHERE \"tx_hash\" IS NOT NULL\n `);\n }\n}\n","export { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nexport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\nexport { AddJournalIdempotencyIndex1747500000000 } from \"./1747500000000-AddJournalIdempotencyIndex\";\nexport { AddLockedMintCompositeIndexes1747600000000 } from \"./1747600000000-AddLockedMintCompositeIndexes\";\nexport { FixIdempotencyAddTokenAddress1747700000000 } from \"./1747700000000-FixIdempotencyAddTokenAddress\";\n\nimport { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nimport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\nimport { AddJournalIdempotencyIndex1747500000000 } from \"./1747500000000-AddJournalIdempotencyIndex\";\nimport { AddLockedMintCompositeIndexes1747600000000 } from \"./1747600000000-AddLockedMintCompositeIndexes\";\nimport { FixIdempotencyAddTokenAddress1747700000000 } from \"./1747700000000-FixIdempotencyAddTokenAddress\";\n\n/**\n * All shipped migrations in chronological order. Drop into TypeORM's\n * `migrations` config:\n *\n * import { PAFI_MIGRATIONS } from \"@pafi-dev/issuer-postgres/migrations\";\n *\n * new DataSource({\n * entities: [...PAFI_ENTITIES, ...yourCustomEntities],\n * migrations: [...PAFI_MIGRATIONS, ...yourCustomMigrations],\n * });\n */\nexport const PAFI_MIGRATIONS = [\n InitialSchema1700000000000,\n CreateRedemptionHistory1746230400001,\n AddJournalIdempotencyIndex1747500000000,\n AddLockedMintCompositeIndexes1747600000000,\n FixIdempotencyAddTokenAddress1747700000000,\n] as const;\n"],"mappings":"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","names":["Column","CreateDateColumn","Entity","Index","PrimaryGeneratedColumn","PrimaryGeneratedColumn","Column","CreateDateColumn","Entity","Index","Column","Entity","Column","Entity","Column","CreateDateColumn","Entity","Index","PrimaryGeneratedColumn","PrimaryGeneratedColumn","Column","CreateDateColumn","Entity","Index","Column","Entity","PrimaryColumn","UpdateDateColumn","PrimaryColumn","Column","UpdateDateColumn","Entity","getAddress","Column","CreateDateColumn","Entity","Index","PrimaryGeneratedColumn","PrimaryGeneratedColumn","Column","CreateDateColumn","Entity","Index","getAddress"]}
@@ -20,7 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/migrations/index.ts
21
21
  var migrations_exports = {};
22
22
  __export(migrations_exports, {
23
+ AddJournalIdempotencyIndex1747500000000: () => AddJournalIdempotencyIndex1747500000000,
24
+ AddLockedMintCompositeIndexes1747600000000: () => AddLockedMintCompositeIndexes1747600000000,
23
25
  CreateRedemptionHistory1746230400001: () => CreateRedemptionHistory1746230400001,
26
+ FixIdempotencyAddTokenAddress1747700000000: () => FixIdempotencyAddTokenAddress1747700000000,
24
27
  InitialSchema1700000000000: () => InitialSchema1700000000000,
25
28
  PAFI_MIGRATIONS: () => PAFI_MIGRATIONS
26
29
  });
@@ -158,14 +161,110 @@ var CreateRedemptionHistory1746230400001 = class {
158
161
  }
159
162
  };
160
163
 
164
+ // src/migrations/1747500000000-AddJournalIdempotencyIndex.ts
165
+ var AddJournalIdempotencyIndex1747500000000 = class {
166
+ name = "AddJournalIdempotencyIndex1747500000000";
167
+ async up(queryRunner) {
168
+ await queryRunner.query(`
169
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
170
+ ON "ledger_journal" ("user_address", "tx_hash", "reason")
171
+ WHERE "tx_hash" IS NOT NULL
172
+ `);
173
+ }
174
+ async down(queryRunner) {
175
+ await queryRunner.query(
176
+ `DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
177
+ );
178
+ }
179
+ };
180
+
181
+ // src/migrations/1747600000000-AddLockedMintCompositeIndexes.ts
182
+ var AddLockedMintCompositeIndexes1747600000000 = class {
183
+ name = "AddLockedMintCompositeIndexes1747600000000";
184
+ /**
185
+ * CONCURRENTLY index DDL cannot run inside a transaction. Tell
186
+ * TypeORM to issue these statements directly.
187
+ */
188
+ transaction = false;
189
+ async up(queryRunner) {
190
+ await queryRunner.query(`
191
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
192
+ "IDX_locked_mint_user_token_status_expires"
193
+ ON "locked_mint_requests"
194
+ ("user_address", "token_address", "status", "expires_at")
195
+ `);
196
+ await queryRunner.query(`
197
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
198
+ "IDX_locked_mint_user_token_amount_status"
199
+ ON "locked_mint_requests"
200
+ ("user_address", "token_address", "amount", "status")
201
+ `);
202
+ await queryRunner.query(`
203
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
204
+ "IDX_locked_mint_pending_expires"
205
+ ON "locked_mint_requests" ("expires_at")
206
+ WHERE "status" = 'PENDING'
207
+ `);
208
+ await queryRunner.query(
209
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_status"`
210
+ );
211
+ }
212
+ async down(queryRunner) {
213
+ await queryRunner.query(`
214
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_locked_mint_user_status"
215
+ ON "locked_mint_requests" ("user_address", "token_address", "status")
216
+ `);
217
+ await queryRunner.query(
218
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_pending_expires"`
219
+ );
220
+ await queryRunner.query(
221
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_amount_status"`
222
+ );
223
+ await queryRunner.query(
224
+ `DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_status_expires"`
225
+ );
226
+ }
227
+ };
228
+
229
+ // src/migrations/1747700000000-FixIdempotencyAddTokenAddress.ts
230
+ var FixIdempotencyAddTokenAddress1747700000000 = class {
231
+ name = "FixIdempotencyAddTokenAddress1747700000000";
232
+ async up(queryRunner) {
233
+ await queryRunner.query(
234
+ `DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
235
+ );
236
+ await queryRunner.query(`
237
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_token_tx_reason"
238
+ ON "ledger_journal" ("user_address", "token_address", "tx_hash", "reason")
239
+ WHERE "tx_hash" IS NOT NULL
240
+ `);
241
+ }
242
+ async down(queryRunner) {
243
+ await queryRunner.query(
244
+ `DROP INDEX IF EXISTS "UQ_ledger_journal_user_token_tx_reason"`
245
+ );
246
+ await queryRunner.query(`
247
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
248
+ ON "ledger_journal" ("user_address", "tx_hash", "reason")
249
+ WHERE "tx_hash" IS NOT NULL
250
+ `);
251
+ }
252
+ };
253
+
161
254
  // src/migrations/index.ts
162
255
  var PAFI_MIGRATIONS = [
163
256
  InitialSchema1700000000000,
164
- CreateRedemptionHistory1746230400001
257
+ CreateRedemptionHistory1746230400001,
258
+ AddJournalIdempotencyIndex1747500000000,
259
+ AddLockedMintCompositeIndexes1747600000000,
260
+ FixIdempotencyAddTokenAddress1747700000000
165
261
  ];
166
262
  // Annotate the CommonJS export names for ESM import in node:
167
263
  0 && (module.exports = {
264
+ AddJournalIdempotencyIndex1747500000000,
265
+ AddLockedMintCompositeIndexes1747600000000,
168
266
  CreateRedemptionHistory1746230400001,
267
+ FixIdempotencyAddTokenAddress1747700000000,
169
268
  InitialSchema1700000000000,
170
269
  PAFI_MIGRATIONS
171
270
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/index.ts","../../src/migrations/1700000000000-InitialSchema.ts","../../src/migrations/1746230400001-CreateRedemptionHistory.ts"],"sourcesContent":["export { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nexport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\n\nimport { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nimport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\n\n/**\n * All shipped migrations in chronological order. Drop into TypeORM's\n * `migrations` config:\n *\n * import { PAFI_MIGRATIONS } from \"@pafi-dev/issuer-postgres/migrations\";\n *\n * new DataSource({\n * entities: [...PAFI_ENTITIES, ...yourCustomEntities],\n * migrations: [...PAFI_MIGRATIONS, ...yourCustomMigrations],\n * });\n */\nexport const PAFI_MIGRATIONS = [\n InitialSchema1700000000000,\n CreateRedemptionHistory1746230400001,\n] as const;\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Single consolidated initial schema for `@pafi-dev/issuer-postgres`.\n *\n * Combines what gg56 split into two migrations (`InitialSchema` +\n * `AddPendingCredits`) plus the `user_op_hash` column, since this is\n * the v1.4-and-after baseline. Issuers adopting the SDK from scratch\n * apply this once.\n *\n * Tables:\n * user_balances — off-chain point balance per (user, token)\n * locked_mint_requests — reservations during mint flow\n * pending_credits — reserved credits during burn/redeem flow\n * ledger_journal — append-only audit trail of every delta\n * indexer_cursors — PointIndexer / BurnIndexer block cursors\n *\n * Issuer-specific extensions (campaign rules, KYC tables, custom\n * scenarios) belong in a follow-up migration — never edit this file\n * in place once it ships.\n */\nexport class InitialSchema1700000000000 implements MigrationInterface {\n name = \"InitialSchema1700000000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS \"pgcrypto\"`);\n\n // ─── user_balances ──────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"user_balances\" (\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"balance\" numeric(78, 0) NOT NULL DEFAULT 0,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_user_balances\" PRIMARY KEY (\"user_address\", \"token_address\")\n )\n `);\n\n // ─── locked_mint_requests ───────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"locked_mint_requests\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n CONSTRAINT \"PK_locked_mint_requests\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_status\"\n ON \"locked_mint_requests\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_op_hash\"\n ON \"locked_mint_requests\" (\"user_op_hash\")\n `);\n\n // ─── pending_credits ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"pending_credits\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"resolved_at\" TIMESTAMP WITH TIME ZONE,\n CONSTRAINT \"PK_pending_credits\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_status\"\n ON \"pending_credits\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_tx_hash\"\n ON \"pending_credits\" (\"tx_hash\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_op_hash\"\n ON \"pending_credits\" (\"user_op_hash\")\n `);\n\n // ─── ledger_journal ─────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"ledger_journal\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"delta\" numeric(78, 0) NOT NULL,\n \"reason\" varchar(128) NOT NULL,\n \"tx_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_ledger_journal\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_ledger_journal_user_created\"\n ON \"ledger_journal\" (\"user_address\", \"token_address\", \"created_at\")\n `);\n\n // ─── indexer_cursors ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"indexer_cursors\" (\n \"id\" varchar(64) NOT NULL,\n \"next_block\" numeric(78, 0) NOT NULL,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_indexer_cursors\" PRIMARY KEY (\"id\")\n )\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`DROP TABLE \"indexer_cursors\"`);\n await queryRunner.query(`DROP INDEX \"IDX_ledger_journal_user_created\"`);\n await queryRunner.query(`DROP TABLE \"ledger_journal\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_tx_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_status\"`);\n await queryRunner.query(`DROP TABLE \"pending_credits\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_status\"`);\n await queryRunner.query(`DROP TABLE \"locked_mint_requests\"`);\n await queryRunner.query(`DROP TABLE \"user_balances\"`);\n }\n}\n","import { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Adds the `redemption_history` table — append-only log of successful\n * redemption initiates, indexed by (user, time) for the daily-limit\n * SUM() query.\n *\n * This is INDEPENDENT from the main InitialSchema (different timestamp,\n * later than 1700000000000). Issuers who already deployed InitialSchema\n * apply this on top.\n */\nexport class CreateRedemptionHistory1746230400001 implements MigrationInterface {\n name = \"CreateRedemptionHistory1746230400001\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`\n CREATE TABLE IF NOT EXISTS redemption_history (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_address varchar(42) NOT NULL,\n token_address varchar(42),\n amount_pt numeric(78, 0) NOT NULL,\n created_at_unix_sec bigint NOT NULL,\n reservation_id varchar(64),\n row_created_at timestamptz NOT NULL DEFAULT NOW(),\n CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)\n )\n `);\n\n await queryRunner.query(`\n CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created\n ON redemption_history (user_address, created_at_unix_sec DESC)\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS idx_redemption_history_user_created`,\n );\n await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACqBO,IAAM,6BAAN,MAA+D;AAAA,EACpE,OAAO;AAAA,EAEP,MAAa,GAAG,aAAyC;AACvD,UAAM,YAAY,MAAM,2CAA2C;AAGnE,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAQvB;AAGD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAavB;AACD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AACD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AAGD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAcvB;AACD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AACD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AACD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AAGD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAWvB;AACD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AAGD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAOvB;AAAA,EACH;AAAA,EAEA,MAAa,KAAK,aAAyC;AACzD,UAAM,YAAY,MAAM,8BAA8B;AACtD,UAAM,YAAY,MAAM,8CAA8C;AACtE,UAAM,YAAY,MAAM,6BAA6B;AACrD,UAAM,YAAY,MAAM,+CAA+C;AACvE,UAAM,YAAY,MAAM,0CAA0C;AAClE,UAAM,YAAY,MAAM,8CAA8C;AACtE,UAAM,YAAY,MAAM,8BAA8B;AACtD,UAAM,YAAY,MAAM,2CAA2C;AACnE,UAAM,YAAY,MAAM,0CAA0C;AAClE,UAAM,YAAY,MAAM,mCAAmC;AAC3D,UAAM,YAAY,MAAM,4BAA4B;AAAA,EACtD;AACF;;;AC1HO,IAAM,uCAAN,MAAyE;AAAA,EAC9E,OAAO;AAAA,EAEP,MAAa,GAAG,aAAyC;AACvD,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAWvB;AAED,UAAM,YAAY,MAAM;AAAA;AAAA;AAAA,KAGvB;AAAA,EACH;AAAA,EAEA,MAAa,KAAK,aAAyC;AACzD,UAAM,YAAY;AAAA,MAChB;AAAA,IACF;AACA,UAAM,YAAY,MAAM,yCAAyC;AAAA,EACnE;AACF;;;AFvBO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/migrations/index.ts","../../src/migrations/1700000000000-InitialSchema.ts","../../src/migrations/1746230400001-CreateRedemptionHistory.ts","../../src/migrations/1747500000000-AddJournalIdempotencyIndex.ts","../../src/migrations/1747600000000-AddLockedMintCompositeIndexes.ts","../../src/migrations/1747700000000-FixIdempotencyAddTokenAddress.ts"],"sourcesContent":["export { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nexport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\nexport { AddJournalIdempotencyIndex1747500000000 } from \"./1747500000000-AddJournalIdempotencyIndex\";\nexport { AddLockedMintCompositeIndexes1747600000000 } from \"./1747600000000-AddLockedMintCompositeIndexes\";\nexport { FixIdempotencyAddTokenAddress1747700000000 } from \"./1747700000000-FixIdempotencyAddTokenAddress\";\n\nimport { InitialSchema1700000000000 } from \"./1700000000000-InitialSchema\";\nimport { CreateRedemptionHistory1746230400001 } from \"./1746230400001-CreateRedemptionHistory\";\nimport { AddJournalIdempotencyIndex1747500000000 } from \"./1747500000000-AddJournalIdempotencyIndex\";\nimport { AddLockedMintCompositeIndexes1747600000000 } from \"./1747600000000-AddLockedMintCompositeIndexes\";\nimport { FixIdempotencyAddTokenAddress1747700000000 } from \"./1747700000000-FixIdempotencyAddTokenAddress\";\n\n/**\n * All shipped migrations in chronological order. Drop into TypeORM's\n * `migrations` config:\n *\n * import { PAFI_MIGRATIONS } from \"@pafi-dev/issuer-postgres/migrations\";\n *\n * new DataSource({\n * entities: [...PAFI_ENTITIES, ...yourCustomEntities],\n * migrations: [...PAFI_MIGRATIONS, ...yourCustomMigrations],\n * });\n */\nexport const PAFI_MIGRATIONS = [\n InitialSchema1700000000000,\n CreateRedemptionHistory1746230400001,\n AddJournalIdempotencyIndex1747500000000,\n AddLockedMintCompositeIndexes1747600000000,\n FixIdempotencyAddTokenAddress1747700000000,\n] as const;\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Single consolidated initial schema for `@pafi-dev/issuer-postgres`.\n *\n * Issuers adopting the SDK from scratch apply this migration once;\n * subsequent schema changes ship as follow-up migrations.\n *\n * Tables:\n * user_balances — off-chain point balance per (user, token)\n * locked_mint_requests — reservations during mint flow\n * pending_credits — reserved credits during burn/redeem flow\n * ledger_journal — append-only audit trail of every delta\n * indexer_cursors — PointIndexer / BurnIndexer block cursors\n *\n * Issuer-specific extensions (campaign rules, KYC tables, custom\n * scenarios) belong in a follow-up migration — never edit this file\n * in place once it ships.\n */\nexport class InitialSchema1700000000000 implements MigrationInterface {\n name = \"InitialSchema1700000000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS \"pgcrypto\"`);\n\n // ─── user_balances ──────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"user_balances\" (\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"balance\" numeric(78, 0) NOT NULL DEFAULT 0,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_user_balances\" PRIMARY KEY (\"user_address\", \"token_address\")\n )\n `);\n\n // ─── locked_mint_requests ───────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"locked_mint_requests\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n CONSTRAINT \"PK_locked_mint_requests\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_status\"\n ON \"locked_mint_requests\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_locked_mint_user_op_hash\"\n ON \"locked_mint_requests\" (\"user_op_hash\")\n `);\n\n // ─── pending_credits ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"pending_credits\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"amount\" numeric(78, 0) NOT NULL,\n \"status\" varchar(16) NOT NULL DEFAULT 'PENDING',\n \"tx_hash\" varchar(66),\n \"user_op_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n \"expires_at\" TIMESTAMP WITH TIME ZONE NOT NULL,\n \"resolved_at\" TIMESTAMP WITH TIME ZONE,\n CONSTRAINT \"PK_pending_credits\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_status\"\n ON \"pending_credits\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_tx_hash\"\n ON \"pending_credits\" (\"tx_hash\")\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_pending_credits_user_op_hash\"\n ON \"pending_credits\" (\"user_op_hash\")\n `);\n\n // ─── ledger_journal ─────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"ledger_journal\" (\n \"id\" uuid NOT NULL DEFAULT gen_random_uuid(),\n \"user_address\" varchar(42) NOT NULL,\n \"token_address\" varchar(42) NOT NULL,\n \"delta\" numeric(78, 0) NOT NULL,\n \"reason\" varchar(128) NOT NULL,\n \"tx_hash\" varchar(66),\n \"created_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_ledger_journal\" PRIMARY KEY (\"id\")\n )\n `);\n await queryRunner.query(`\n CREATE INDEX \"IDX_ledger_journal_user_created\"\n ON \"ledger_journal\" (\"user_address\", \"token_address\", \"created_at\")\n `);\n\n // ─── indexer_cursors ────────────────────────────────────────────\n await queryRunner.query(`\n CREATE TABLE \"indexer_cursors\" (\n \"id\" varchar(64) NOT NULL,\n \"next_block\" numeric(78, 0) NOT NULL,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT now(),\n CONSTRAINT \"PK_indexer_cursors\" PRIMARY KEY (\"id\")\n )\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`DROP TABLE \"indexer_cursors\"`);\n await queryRunner.query(`DROP INDEX \"IDX_ledger_journal_user_created\"`);\n await queryRunner.query(`DROP TABLE \"ledger_journal\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_tx_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_pending_credits_user_status\"`);\n await queryRunner.query(`DROP TABLE \"pending_credits\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_op_hash\"`);\n await queryRunner.query(`DROP INDEX \"IDX_locked_mint_user_status\"`);\n await queryRunner.query(`DROP TABLE \"locked_mint_requests\"`);\n await queryRunner.query(`DROP TABLE \"user_balances\"`);\n }\n}\n","import { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Adds the `redemption_history` table — append-only log of successful\n * redemption initiates, indexed by (user, time) for the daily-limit\n * SUM() query.\n *\n * This is INDEPENDENT from the main InitialSchema (different timestamp,\n * later than 1700000000000). Issuers who already deployed InitialSchema\n * apply this on top.\n */\nexport class CreateRedemptionHistory1746230400001 implements MigrationInterface {\n name = \"CreateRedemptionHistory1746230400001\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`\n CREATE TABLE IF NOT EXISTS redemption_history (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n user_address varchar(42) NOT NULL,\n token_address varchar(42),\n amount_pt numeric(78, 0) NOT NULL,\n created_at_unix_sec bigint NOT NULL,\n reservation_id varchar(64),\n row_created_at timestamptz NOT NULL DEFAULT NOW(),\n CONSTRAINT redemption_history_amount_positive CHECK (amount_pt > 0)\n )\n `);\n\n await queryRunner.query(`\n CREATE INDEX IF NOT EXISTS idx_redemption_history_user_created\n ON redemption_history (user_address, created_at_unix_sec DESC)\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS idx_redemption_history_user_created`,\n );\n await queryRunner.query(`DROP TABLE IF EXISTS redemption_history`);\n }\n}\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Adds a partial unique index on `ledger_journal` to enforce\n * idempotency for indexer-driven reasons:\n *\n * UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL\n *\n * Last-line defense against double-deduct / double-credit when the\n * mint or burn indexer replays the same on-chain event (reorg, pod\n * restart, duplicate replica). Rows with `tx_hash IS NULL` (off-chain\n * credits like AIRDROP, refunds) are unaffected by the partial\n * predicate.\n *\n * Pre-flight: existing duplicate `(user_address, tx_hash, reason)`\n * rows with non-null `tx_hash` will block index creation. Operators\n * who suspect prior double-processing should reconcile manually\n * before applying. The query below surfaces offenders:\n *\n * SELECT user_address, tx_hash, reason, COUNT(*)\n * FROM ledger_journal\n * WHERE tx_hash IS NOT NULL\n * GROUP BY user_address, tx_hash, reason\n * HAVING COUNT(*) > 1;\n */\nexport class AddJournalIdempotencyIndex1747500000000\n implements MigrationInterface\n{\n name = \"AddJournalIdempotencyIndex1747500000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_ledger_journal_user_tx_reason\"\n ON \"ledger_journal\" (\"user_address\", \"tx_hash\", \"reason\")\n WHERE \"tx_hash\" IS NOT NULL\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS \"UQ_ledger_journal_user_tx_reason\"`,\n );\n }\n}\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Replaces the narrow `IDX_locked_mint_user_status` (3 cols:\n * `user_address`, `token_address`, `status`) with three composite\n * indexes that match the actual query shapes used by the SDK.\n *\n * Index rationale:\n *\n * `IDX_locked_mint_user_token_status_expires`\n * Covers `sumPendingLocks` (called inside every `getBalance` +\n * `lockForMinting`). The 4-col index lets Postgres push\n * `expires_at > NOW()` into the index range scan instead of\n * post-filtering rows from the heap. Without it, unswept\n * expired locks cause a latency cliff under load.\n *\n * `IDX_locked_mint_user_token_amount_status`\n * Covers `PointIndexer.pickMatchingLock` and the lock-resolution\n * `findOne` inside `deductBalance`. The amount column is the\n * selectivity-critical predicate (PENDING locks for the same\n * user/token often share a token but differ by amount).\n *\n * `IDX_locked_mint_pending_expires` (partial)\n * Covers the `markExpiredLocks` sweep. Partial predicate\n * `WHERE status = 'PENDING'` keeps the index small — terminal\n * rows (`MINTED` / `EXPIRED` / `FAILED`) drop out automatically.\n *\n * --- Operational notes ---\n *\n * `CREATE INDEX CONCURRENTLY` runs without a write lock on the table\n * — safe on production traffic — but cannot run inside a transaction\n * block. `transaction = false` disables TypeORM's per-migration\n * transaction wrapping. Trade-off: if this migration partially\n * fails, one or more indexes may be left in an INVALID state\n * (`pg_index.indisvalid = false`). Recovery:\n *\n * -- find half-built indexes\n * SELECT i.relname AS index_name\n * FROM pg_index x\n * JOIN pg_class i ON i.oid = x.indexrelid\n * JOIN pg_class t ON t.oid = x.indrelid\n * WHERE t.relname = 'locked_mint_requests' AND NOT x.indisvalid;\n *\n * -- drop and rerun the migration\n * DROP INDEX CONCURRENTLY IF EXISTS \"<index_name>\";\n *\n * On large tables (10M+ rows) each `CREATE INDEX CONCURRENTLY` can\n * take several minutes — it makes two table passes plus a final\n * synchronisation. Plan accordingly.\n */\nexport class AddLockedMintCompositeIndexes1747600000000\n implements MigrationInterface\n{\n name = \"AddLockedMintCompositeIndexes1747600000000\";\n\n /**\n * CONCURRENTLY index DDL cannot run inside a transaction. Tell\n * TypeORM to issue these statements directly.\n */\n transaction = false as const;\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n // Order matters: create the replacement indexes BEFORE dropping\n // the narrow one, so concurrent reads always have at least one\n // usable index throughout the migration.\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS\n \"IDX_locked_mint_user_token_status_expires\"\n ON \"locked_mint_requests\"\n (\"user_address\", \"token_address\", \"status\", \"expires_at\")\n `);\n\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS\n \"IDX_locked_mint_user_token_amount_status\"\n ON \"locked_mint_requests\"\n (\"user_address\", \"token_address\", \"amount\", \"status\")\n `);\n\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS\n \"IDX_locked_mint_pending_expires\"\n ON \"locked_mint_requests\" (\"expires_at\")\n WHERE \"status\" = 'PENDING'\n `);\n\n // The old `(user_address, token_address, status)` index is a\n // strict prefix of the new 4-col index, so dropping it is safe.\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_user_status\"`,\n );\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n // Restore the original narrow index first, then drop the new\n // ones — same \"always have a usable index\" invariant in reverse.\n await queryRunner.query(`\n CREATE INDEX CONCURRENTLY IF NOT EXISTS \"IDX_locked_mint_user_status\"\n ON \"locked_mint_requests\" (\"user_address\", \"token_address\", \"status\")\n `);\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_pending_expires\"`,\n );\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_user_token_amount_status\"`,\n );\n await queryRunner.query(\n `DROP INDEX CONCURRENTLY IF EXISTS \"IDX_locked_mint_user_token_status_expires\"`,\n );\n }\n}\n","import type { MigrationInterface, QueryRunner } from \"typeorm\";\n\n/**\n * Audit PACI5-7 — Tx-hash idempotency tuple collapses multi-token mints\n * into a single debit.\n *\n * The original idempotency index (`UQ_ledger_journal_user_tx_reason`,\n * shipped in `1747500000000-AddJournalIdempotencyIndex`) was\n *\n * UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL\n *\n * which omits `token_address`. When a single transaction mints two\n * different PointTokens for the same user (legitimate batched\n * EIP-7702 claim, or Pimlico bundler grouping two single-mint\n * UserOps into one bundle), the second indexer's \"already processed?\"\n * lookup matches the first token's journal row and the debit is\n * silently skipped — yet `PointIndexer.finalize` still flips the\n * second lock to MINTED. Result: on-chain mint with no off-chain\n * debit → off-chain balance stays spendable → user can re-mint until\n * the issuer cap is exhausted.\n *\n * This migration replaces the tuple with the corrected one:\n *\n * UNIQUE (user_address, token_address, tx_hash, reason)\n * WHERE tx_hash IS NOT NULL\n *\n * The `deductBalance` \"already\" lookup is updated to include\n * `token_address` in the same release.\n *\n * Pre-flight: the new index is strictly more permissive than the old\n * one (adds a column to the key, never removes), so no existing row\n * pair that was unique under the old index can collide under the new\n * one. Safe to apply on populated databases without reconciliation.\n *\n * Down migration restores the original (vulnerable) index — intended\n * only for emergency rollback. Operators rolling back must also\n * revert the `deductBalance` lookup in `postgresPointLedger.ts`;\n * otherwise the application would query a four-column tuple against\n * a three-column index and idempotency would fail open.\n */\nexport class FixIdempotencyAddTokenAddress1747700000000\n implements MigrationInterface\n{\n name = \"FixIdempotencyAddTokenAddress1747700000000\";\n\n public async up(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS \"UQ_ledger_journal_user_tx_reason\"`,\n );\n await queryRunner.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_ledger_journal_user_token_tx_reason\"\n ON \"ledger_journal\" (\"user_address\", \"token_address\", \"tx_hash\", \"reason\")\n WHERE \"tx_hash\" IS NOT NULL\n `);\n }\n\n public async down(queryRunner: QueryRunner): Promise<void> {\n await queryRunner.query(\n `DROP INDEX IF EXISTS \"UQ_ledger_journal_user_token_tx_reason\"`,\n );\n await queryRunner.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_ledger_journal_user_tx_reason\"\n ON \"ledger_journal\" (\"user_address\", \"tx_hash\", \"reason\")\n WHERE \"tx_hash\" IS NOT NULL\n `);\n }\n}\n"],"mappings":"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","names":[]}