@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/README.md +37 -10
- package/dist/entities/index.cjs +25 -2
- package/dist/entities/index.cjs.map +1 -1
- package/dist/entities/index.d.cts +39 -2
- package/dist/entities/index.d.ts +39 -2
- package/dist/entities/index.js +25 -2
- package/dist/entities/index.js.map +1 -1
- package/dist/index.cjs +203 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +203 -31
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +100 -1
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +136 -6
- package/dist/migrations/index.d.ts +136 -6
- package/dist/migrations/index.js +97 -1
- package/dist/migrations/index.js.map +1 -1
- package/package.json +16 -5
|
@@ -3,10 +3,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
3
3
|
/**
|
|
4
4
|
* Single consolidated initial schema for `@pafi-dev/issuer-postgres`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the v1.4-and-after baseline. Issuers adopting the SDK from scratch
|
|
9
|
-
* apply this once.
|
|
6
|
+
* Issuers adopting the SDK from scratch apply this migration once;
|
|
7
|
+
* subsequent schema changes ship as follow-up migrations.
|
|
10
8
|
*
|
|
11
9
|
* Tables:
|
|
12
10
|
* user_balances — off-chain point balance per (user, token)
|
|
@@ -40,6 +38,138 @@ declare class CreateRedemptionHistory1746230400001 implements MigrationInterface
|
|
|
40
38
|
down(queryRunner: QueryRunner): Promise<void>;
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Adds a partial unique index on `ledger_journal` to enforce
|
|
43
|
+
* idempotency for indexer-driven reasons:
|
|
44
|
+
*
|
|
45
|
+
* UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL
|
|
46
|
+
*
|
|
47
|
+
* Last-line defense against double-deduct / double-credit when the
|
|
48
|
+
* mint or burn indexer replays the same on-chain event (reorg, pod
|
|
49
|
+
* restart, duplicate replica). Rows with `tx_hash IS NULL` (off-chain
|
|
50
|
+
* credits like AIRDROP, refunds) are unaffected by the partial
|
|
51
|
+
* predicate.
|
|
52
|
+
*
|
|
53
|
+
* Pre-flight: existing duplicate `(user_address, tx_hash, reason)`
|
|
54
|
+
* rows with non-null `tx_hash` will block index creation. Operators
|
|
55
|
+
* who suspect prior double-processing should reconcile manually
|
|
56
|
+
* before applying. The query below surfaces offenders:
|
|
57
|
+
*
|
|
58
|
+
* SELECT user_address, tx_hash, reason, COUNT(*)
|
|
59
|
+
* FROM ledger_journal
|
|
60
|
+
* WHERE tx_hash IS NOT NULL
|
|
61
|
+
* GROUP BY user_address, tx_hash, reason
|
|
62
|
+
* HAVING COUNT(*) > 1;
|
|
63
|
+
*/
|
|
64
|
+
declare class AddJournalIdempotencyIndex1747500000000 implements MigrationInterface {
|
|
65
|
+
name: string;
|
|
66
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
67
|
+
down(queryRunner: QueryRunner): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Replaces the narrow `IDX_locked_mint_user_status` (3 cols:
|
|
72
|
+
* `user_address`, `token_address`, `status`) with three composite
|
|
73
|
+
* indexes that match the actual query shapes used by the SDK.
|
|
74
|
+
*
|
|
75
|
+
* Index rationale:
|
|
76
|
+
*
|
|
77
|
+
* `IDX_locked_mint_user_token_status_expires`
|
|
78
|
+
* Covers `sumPendingLocks` (called inside every `getBalance` +
|
|
79
|
+
* `lockForMinting`). The 4-col index lets Postgres push
|
|
80
|
+
* `expires_at > NOW()` into the index range scan instead of
|
|
81
|
+
* post-filtering rows from the heap. Without it, unswept
|
|
82
|
+
* expired locks cause a latency cliff under load.
|
|
83
|
+
*
|
|
84
|
+
* `IDX_locked_mint_user_token_amount_status`
|
|
85
|
+
* Covers `PointIndexer.pickMatchingLock` and the lock-resolution
|
|
86
|
+
* `findOne` inside `deductBalance`. The amount column is the
|
|
87
|
+
* selectivity-critical predicate (PENDING locks for the same
|
|
88
|
+
* user/token often share a token but differ by amount).
|
|
89
|
+
*
|
|
90
|
+
* `IDX_locked_mint_pending_expires` (partial)
|
|
91
|
+
* Covers the `markExpiredLocks` sweep. Partial predicate
|
|
92
|
+
* `WHERE status = 'PENDING'` keeps the index small — terminal
|
|
93
|
+
* rows (`MINTED` / `EXPIRED` / `FAILED`) drop out automatically.
|
|
94
|
+
*
|
|
95
|
+
* --- Operational notes ---
|
|
96
|
+
*
|
|
97
|
+
* `CREATE INDEX CONCURRENTLY` runs without a write lock on the table
|
|
98
|
+
* — safe on production traffic — but cannot run inside a transaction
|
|
99
|
+
* block. `transaction = false` disables TypeORM's per-migration
|
|
100
|
+
* transaction wrapping. Trade-off: if this migration partially
|
|
101
|
+
* fails, one or more indexes may be left in an INVALID state
|
|
102
|
+
* (`pg_index.indisvalid = false`). Recovery:
|
|
103
|
+
*
|
|
104
|
+
* -- find half-built indexes
|
|
105
|
+
* SELECT i.relname AS index_name
|
|
106
|
+
* FROM pg_index x
|
|
107
|
+
* JOIN pg_class i ON i.oid = x.indexrelid
|
|
108
|
+
* JOIN pg_class t ON t.oid = x.indrelid
|
|
109
|
+
* WHERE t.relname = 'locked_mint_requests' AND NOT x.indisvalid;
|
|
110
|
+
*
|
|
111
|
+
* -- drop and rerun the migration
|
|
112
|
+
* DROP INDEX CONCURRENTLY IF EXISTS "<index_name>";
|
|
113
|
+
*
|
|
114
|
+
* On large tables (10M+ rows) each `CREATE INDEX CONCURRENTLY` can
|
|
115
|
+
* take several minutes — it makes two table passes plus a final
|
|
116
|
+
* synchronisation. Plan accordingly.
|
|
117
|
+
*/
|
|
118
|
+
declare class AddLockedMintCompositeIndexes1747600000000 implements MigrationInterface {
|
|
119
|
+
name: string;
|
|
120
|
+
/**
|
|
121
|
+
* CONCURRENTLY index DDL cannot run inside a transaction. Tell
|
|
122
|
+
* TypeORM to issue these statements directly.
|
|
123
|
+
*/
|
|
124
|
+
transaction: false;
|
|
125
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
126
|
+
down(queryRunner: QueryRunner): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Audit PACI5-7 — Tx-hash idempotency tuple collapses multi-token mints
|
|
131
|
+
* into a single debit.
|
|
132
|
+
*
|
|
133
|
+
* The original idempotency index (`UQ_ledger_journal_user_tx_reason`,
|
|
134
|
+
* shipped in `1747500000000-AddJournalIdempotencyIndex`) was
|
|
135
|
+
*
|
|
136
|
+
* UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL
|
|
137
|
+
*
|
|
138
|
+
* which omits `token_address`. When a single transaction mints two
|
|
139
|
+
* different PointTokens for the same user (legitimate batched
|
|
140
|
+
* EIP-7702 claim, or Pimlico bundler grouping two single-mint
|
|
141
|
+
* UserOps into one bundle), the second indexer's "already processed?"
|
|
142
|
+
* lookup matches the first token's journal row and the debit is
|
|
143
|
+
* silently skipped — yet `PointIndexer.finalize` still flips the
|
|
144
|
+
* second lock to MINTED. Result: on-chain mint with no off-chain
|
|
145
|
+
* debit → off-chain balance stays spendable → user can re-mint until
|
|
146
|
+
* the issuer cap is exhausted.
|
|
147
|
+
*
|
|
148
|
+
* This migration replaces the tuple with the corrected one:
|
|
149
|
+
*
|
|
150
|
+
* UNIQUE (user_address, token_address, tx_hash, reason)
|
|
151
|
+
* WHERE tx_hash IS NOT NULL
|
|
152
|
+
*
|
|
153
|
+
* The `deductBalance` "already" lookup is updated to include
|
|
154
|
+
* `token_address` in the same release.
|
|
155
|
+
*
|
|
156
|
+
* Pre-flight: the new index is strictly more permissive than the old
|
|
157
|
+
* one (adds a column to the key, never removes), so no existing row
|
|
158
|
+
* pair that was unique under the old index can collide under the new
|
|
159
|
+
* one. Safe to apply on populated databases without reconciliation.
|
|
160
|
+
*
|
|
161
|
+
* Down migration restores the original (vulnerable) index — intended
|
|
162
|
+
* only for emergency rollback. Operators rolling back must also
|
|
163
|
+
* revert the `deductBalance` lookup in `postgresPointLedger.ts`;
|
|
164
|
+
* otherwise the application would query a four-column tuple against
|
|
165
|
+
* a three-column index and idempotency would fail open.
|
|
166
|
+
*/
|
|
167
|
+
declare class FixIdempotencyAddTokenAddress1747700000000 implements MigrationInterface {
|
|
168
|
+
name: string;
|
|
169
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
170
|
+
down(queryRunner: QueryRunner): Promise<void>;
|
|
171
|
+
}
|
|
172
|
+
|
|
43
173
|
/**
|
|
44
174
|
* All shipped migrations in chronological order. Drop into TypeORM's
|
|
45
175
|
* `migrations` config:
|
|
@@ -51,6 +181,6 @@ declare class CreateRedemptionHistory1746230400001 implements MigrationInterface
|
|
|
51
181
|
* migrations: [...PAFI_MIGRATIONS, ...yourCustomMigrations],
|
|
52
182
|
* });
|
|
53
183
|
*/
|
|
54
|
-
declare const PAFI_MIGRATIONS: readonly [typeof InitialSchema1700000000000, typeof CreateRedemptionHistory1746230400001];
|
|
184
|
+
declare const PAFI_MIGRATIONS: readonly [typeof InitialSchema1700000000000, typeof CreateRedemptionHistory1746230400001, typeof AddJournalIdempotencyIndex1747500000000, typeof AddLockedMintCompositeIndexes1747600000000, typeof FixIdempotencyAddTokenAddress1747700000000];
|
|
55
185
|
|
|
56
|
-
export { CreateRedemptionHistory1746230400001, InitialSchema1700000000000, PAFI_MIGRATIONS };
|
|
186
|
+
export { AddJournalIdempotencyIndex1747500000000, AddLockedMintCompositeIndexes1747600000000, CreateRedemptionHistory1746230400001, FixIdempotencyAddTokenAddress1747700000000, InitialSchema1700000000000, PAFI_MIGRATIONS };
|
|
@@ -3,10 +3,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
3
3
|
/**
|
|
4
4
|
* Single consolidated initial schema for `@pafi-dev/issuer-postgres`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the v1.4-and-after baseline. Issuers adopting the SDK from scratch
|
|
9
|
-
* apply this once.
|
|
6
|
+
* Issuers adopting the SDK from scratch apply this migration once;
|
|
7
|
+
* subsequent schema changes ship as follow-up migrations.
|
|
10
8
|
*
|
|
11
9
|
* Tables:
|
|
12
10
|
* user_balances — off-chain point balance per (user, token)
|
|
@@ -40,6 +38,138 @@ declare class CreateRedemptionHistory1746230400001 implements MigrationInterface
|
|
|
40
38
|
down(queryRunner: QueryRunner): Promise<void>;
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Adds a partial unique index on `ledger_journal` to enforce
|
|
43
|
+
* idempotency for indexer-driven reasons:
|
|
44
|
+
*
|
|
45
|
+
* UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL
|
|
46
|
+
*
|
|
47
|
+
* Last-line defense against double-deduct / double-credit when the
|
|
48
|
+
* mint or burn indexer replays the same on-chain event (reorg, pod
|
|
49
|
+
* restart, duplicate replica). Rows with `tx_hash IS NULL` (off-chain
|
|
50
|
+
* credits like AIRDROP, refunds) are unaffected by the partial
|
|
51
|
+
* predicate.
|
|
52
|
+
*
|
|
53
|
+
* Pre-flight: existing duplicate `(user_address, tx_hash, reason)`
|
|
54
|
+
* rows with non-null `tx_hash` will block index creation. Operators
|
|
55
|
+
* who suspect prior double-processing should reconcile manually
|
|
56
|
+
* before applying. The query below surfaces offenders:
|
|
57
|
+
*
|
|
58
|
+
* SELECT user_address, tx_hash, reason, COUNT(*)
|
|
59
|
+
* FROM ledger_journal
|
|
60
|
+
* WHERE tx_hash IS NOT NULL
|
|
61
|
+
* GROUP BY user_address, tx_hash, reason
|
|
62
|
+
* HAVING COUNT(*) > 1;
|
|
63
|
+
*/
|
|
64
|
+
declare class AddJournalIdempotencyIndex1747500000000 implements MigrationInterface {
|
|
65
|
+
name: string;
|
|
66
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
67
|
+
down(queryRunner: QueryRunner): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Replaces the narrow `IDX_locked_mint_user_status` (3 cols:
|
|
72
|
+
* `user_address`, `token_address`, `status`) with three composite
|
|
73
|
+
* indexes that match the actual query shapes used by the SDK.
|
|
74
|
+
*
|
|
75
|
+
* Index rationale:
|
|
76
|
+
*
|
|
77
|
+
* `IDX_locked_mint_user_token_status_expires`
|
|
78
|
+
* Covers `sumPendingLocks` (called inside every `getBalance` +
|
|
79
|
+
* `lockForMinting`). The 4-col index lets Postgres push
|
|
80
|
+
* `expires_at > NOW()` into the index range scan instead of
|
|
81
|
+
* post-filtering rows from the heap. Without it, unswept
|
|
82
|
+
* expired locks cause a latency cliff under load.
|
|
83
|
+
*
|
|
84
|
+
* `IDX_locked_mint_user_token_amount_status`
|
|
85
|
+
* Covers `PointIndexer.pickMatchingLock` and the lock-resolution
|
|
86
|
+
* `findOne` inside `deductBalance`. The amount column is the
|
|
87
|
+
* selectivity-critical predicate (PENDING locks for the same
|
|
88
|
+
* user/token often share a token but differ by amount).
|
|
89
|
+
*
|
|
90
|
+
* `IDX_locked_mint_pending_expires` (partial)
|
|
91
|
+
* Covers the `markExpiredLocks` sweep. Partial predicate
|
|
92
|
+
* `WHERE status = 'PENDING'` keeps the index small — terminal
|
|
93
|
+
* rows (`MINTED` / `EXPIRED` / `FAILED`) drop out automatically.
|
|
94
|
+
*
|
|
95
|
+
* --- Operational notes ---
|
|
96
|
+
*
|
|
97
|
+
* `CREATE INDEX CONCURRENTLY` runs without a write lock on the table
|
|
98
|
+
* — safe on production traffic — but cannot run inside a transaction
|
|
99
|
+
* block. `transaction = false` disables TypeORM's per-migration
|
|
100
|
+
* transaction wrapping. Trade-off: if this migration partially
|
|
101
|
+
* fails, one or more indexes may be left in an INVALID state
|
|
102
|
+
* (`pg_index.indisvalid = false`). Recovery:
|
|
103
|
+
*
|
|
104
|
+
* -- find half-built indexes
|
|
105
|
+
* SELECT i.relname AS index_name
|
|
106
|
+
* FROM pg_index x
|
|
107
|
+
* JOIN pg_class i ON i.oid = x.indexrelid
|
|
108
|
+
* JOIN pg_class t ON t.oid = x.indrelid
|
|
109
|
+
* WHERE t.relname = 'locked_mint_requests' AND NOT x.indisvalid;
|
|
110
|
+
*
|
|
111
|
+
* -- drop and rerun the migration
|
|
112
|
+
* DROP INDEX CONCURRENTLY IF EXISTS "<index_name>";
|
|
113
|
+
*
|
|
114
|
+
* On large tables (10M+ rows) each `CREATE INDEX CONCURRENTLY` can
|
|
115
|
+
* take several minutes — it makes two table passes plus a final
|
|
116
|
+
* synchronisation. Plan accordingly.
|
|
117
|
+
*/
|
|
118
|
+
declare class AddLockedMintCompositeIndexes1747600000000 implements MigrationInterface {
|
|
119
|
+
name: string;
|
|
120
|
+
/**
|
|
121
|
+
* CONCURRENTLY index DDL cannot run inside a transaction. Tell
|
|
122
|
+
* TypeORM to issue these statements directly.
|
|
123
|
+
*/
|
|
124
|
+
transaction: false;
|
|
125
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
126
|
+
down(queryRunner: QueryRunner): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Audit PACI5-7 — Tx-hash idempotency tuple collapses multi-token mints
|
|
131
|
+
* into a single debit.
|
|
132
|
+
*
|
|
133
|
+
* The original idempotency index (`UQ_ledger_journal_user_tx_reason`,
|
|
134
|
+
* shipped in `1747500000000-AddJournalIdempotencyIndex`) was
|
|
135
|
+
*
|
|
136
|
+
* UNIQUE (user_address, tx_hash, reason) WHERE tx_hash IS NOT NULL
|
|
137
|
+
*
|
|
138
|
+
* which omits `token_address`. When a single transaction mints two
|
|
139
|
+
* different PointTokens for the same user (legitimate batched
|
|
140
|
+
* EIP-7702 claim, or Pimlico bundler grouping two single-mint
|
|
141
|
+
* UserOps into one bundle), the second indexer's "already processed?"
|
|
142
|
+
* lookup matches the first token's journal row and the debit is
|
|
143
|
+
* silently skipped — yet `PointIndexer.finalize` still flips the
|
|
144
|
+
* second lock to MINTED. Result: on-chain mint with no off-chain
|
|
145
|
+
* debit → off-chain balance stays spendable → user can re-mint until
|
|
146
|
+
* the issuer cap is exhausted.
|
|
147
|
+
*
|
|
148
|
+
* This migration replaces the tuple with the corrected one:
|
|
149
|
+
*
|
|
150
|
+
* UNIQUE (user_address, token_address, tx_hash, reason)
|
|
151
|
+
* WHERE tx_hash IS NOT NULL
|
|
152
|
+
*
|
|
153
|
+
* The `deductBalance` "already" lookup is updated to include
|
|
154
|
+
* `token_address` in the same release.
|
|
155
|
+
*
|
|
156
|
+
* Pre-flight: the new index is strictly more permissive than the old
|
|
157
|
+
* one (adds a column to the key, never removes), so no existing row
|
|
158
|
+
* pair that was unique under the old index can collide under the new
|
|
159
|
+
* one. Safe to apply on populated databases without reconciliation.
|
|
160
|
+
*
|
|
161
|
+
* Down migration restores the original (vulnerable) index — intended
|
|
162
|
+
* only for emergency rollback. Operators rolling back must also
|
|
163
|
+
* revert the `deductBalance` lookup in `postgresPointLedger.ts`;
|
|
164
|
+
* otherwise the application would query a four-column tuple against
|
|
165
|
+
* a three-column index and idempotency would fail open.
|
|
166
|
+
*/
|
|
167
|
+
declare class FixIdempotencyAddTokenAddress1747700000000 implements MigrationInterface {
|
|
168
|
+
name: string;
|
|
169
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
170
|
+
down(queryRunner: QueryRunner): Promise<void>;
|
|
171
|
+
}
|
|
172
|
+
|
|
43
173
|
/**
|
|
44
174
|
* All shipped migrations in chronological order. Drop into TypeORM's
|
|
45
175
|
* `migrations` config:
|
|
@@ -51,6 +181,6 @@ declare class CreateRedemptionHistory1746230400001 implements MigrationInterface
|
|
|
51
181
|
* migrations: [...PAFI_MIGRATIONS, ...yourCustomMigrations],
|
|
52
182
|
* });
|
|
53
183
|
*/
|
|
54
|
-
declare const PAFI_MIGRATIONS: readonly [typeof InitialSchema1700000000000, typeof CreateRedemptionHistory1746230400001];
|
|
184
|
+
declare const PAFI_MIGRATIONS: readonly [typeof InitialSchema1700000000000, typeof CreateRedemptionHistory1746230400001, typeof AddJournalIdempotencyIndex1747500000000, typeof AddLockedMintCompositeIndexes1747600000000, typeof FixIdempotencyAddTokenAddress1747700000000];
|
|
55
185
|
|
|
56
|
-
export { CreateRedemptionHistory1746230400001, InitialSchema1700000000000, PAFI_MIGRATIONS };
|
|
186
|
+
export { AddJournalIdempotencyIndex1747500000000, AddLockedMintCompositeIndexes1747600000000, CreateRedemptionHistory1746230400001, FixIdempotencyAddTokenAddress1747700000000, InitialSchema1700000000000, PAFI_MIGRATIONS };
|
package/dist/migrations/index.js
CHANGED
|
@@ -130,13 +130,109 @@ var CreateRedemptionHistory1746230400001 = class {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
+
// src/migrations/1747500000000-AddJournalIdempotencyIndex.ts
|
|
134
|
+
var AddJournalIdempotencyIndex1747500000000 = class {
|
|
135
|
+
name = "AddJournalIdempotencyIndex1747500000000";
|
|
136
|
+
async up(queryRunner) {
|
|
137
|
+
await queryRunner.query(`
|
|
138
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
|
|
139
|
+
ON "ledger_journal" ("user_address", "tx_hash", "reason")
|
|
140
|
+
WHERE "tx_hash" IS NOT NULL
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
async down(queryRunner) {
|
|
144
|
+
await queryRunner.query(
|
|
145
|
+
`DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/migrations/1747600000000-AddLockedMintCompositeIndexes.ts
|
|
151
|
+
var AddLockedMintCompositeIndexes1747600000000 = class {
|
|
152
|
+
name = "AddLockedMintCompositeIndexes1747600000000";
|
|
153
|
+
/**
|
|
154
|
+
* CONCURRENTLY index DDL cannot run inside a transaction. Tell
|
|
155
|
+
* TypeORM to issue these statements directly.
|
|
156
|
+
*/
|
|
157
|
+
transaction = false;
|
|
158
|
+
async up(queryRunner) {
|
|
159
|
+
await queryRunner.query(`
|
|
160
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
161
|
+
"IDX_locked_mint_user_token_status_expires"
|
|
162
|
+
ON "locked_mint_requests"
|
|
163
|
+
("user_address", "token_address", "status", "expires_at")
|
|
164
|
+
`);
|
|
165
|
+
await queryRunner.query(`
|
|
166
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
167
|
+
"IDX_locked_mint_user_token_amount_status"
|
|
168
|
+
ON "locked_mint_requests"
|
|
169
|
+
("user_address", "token_address", "amount", "status")
|
|
170
|
+
`);
|
|
171
|
+
await queryRunner.query(`
|
|
172
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
173
|
+
"IDX_locked_mint_pending_expires"
|
|
174
|
+
ON "locked_mint_requests" ("expires_at")
|
|
175
|
+
WHERE "status" = 'PENDING'
|
|
176
|
+
`);
|
|
177
|
+
await queryRunner.query(
|
|
178
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_status"`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async down(queryRunner) {
|
|
182
|
+
await queryRunner.query(`
|
|
183
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_locked_mint_user_status"
|
|
184
|
+
ON "locked_mint_requests" ("user_address", "token_address", "status")
|
|
185
|
+
`);
|
|
186
|
+
await queryRunner.query(
|
|
187
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_pending_expires"`
|
|
188
|
+
);
|
|
189
|
+
await queryRunner.query(
|
|
190
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_amount_status"`
|
|
191
|
+
);
|
|
192
|
+
await queryRunner.query(
|
|
193
|
+
`DROP INDEX CONCURRENTLY IF EXISTS "IDX_locked_mint_user_token_status_expires"`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/migrations/1747700000000-FixIdempotencyAddTokenAddress.ts
|
|
199
|
+
var FixIdempotencyAddTokenAddress1747700000000 = class {
|
|
200
|
+
name = "FixIdempotencyAddTokenAddress1747700000000";
|
|
201
|
+
async up(queryRunner) {
|
|
202
|
+
await queryRunner.query(
|
|
203
|
+
`DROP INDEX IF EXISTS "UQ_ledger_journal_user_tx_reason"`
|
|
204
|
+
);
|
|
205
|
+
await queryRunner.query(`
|
|
206
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_token_tx_reason"
|
|
207
|
+
ON "ledger_journal" ("user_address", "token_address", "tx_hash", "reason")
|
|
208
|
+
WHERE "tx_hash" IS NOT NULL
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
async down(queryRunner) {
|
|
212
|
+
await queryRunner.query(
|
|
213
|
+
`DROP INDEX IF EXISTS "UQ_ledger_journal_user_token_tx_reason"`
|
|
214
|
+
);
|
|
215
|
+
await queryRunner.query(`
|
|
216
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_ledger_journal_user_tx_reason"
|
|
217
|
+
ON "ledger_journal" ("user_address", "tx_hash", "reason")
|
|
218
|
+
WHERE "tx_hash" IS NOT NULL
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
133
223
|
// src/migrations/index.ts
|
|
134
224
|
var PAFI_MIGRATIONS = [
|
|
135
225
|
InitialSchema1700000000000,
|
|
136
|
-
CreateRedemptionHistory1746230400001
|
|
226
|
+
CreateRedemptionHistory1746230400001,
|
|
227
|
+
AddJournalIdempotencyIndex1747500000000,
|
|
228
|
+
AddLockedMintCompositeIndexes1747600000000,
|
|
229
|
+
FixIdempotencyAddTokenAddress1747700000000
|
|
137
230
|
];
|
|
138
231
|
export {
|
|
232
|
+
AddJournalIdempotencyIndex1747500000000,
|
|
233
|
+
AddLockedMintCompositeIndexes1747600000000,
|
|
139
234
|
CreateRedemptionHistory1746230400001,
|
|
235
|
+
FixIdempotencyAddTokenAddress1747700000000,
|
|
140
236
|
InitialSchema1700000000000,
|
|
141
237
|
PAFI_MIGRATIONS
|
|
142
238
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/migrations/1700000000000-InitialSchema.ts","../../src/migrations/1746230400001-CreateRedemptionHistory.ts","../../src/migrations/index.ts"],"sourcesContent":["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":";AAqBO,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;;;ACvBO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../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 { 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":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pafi-dev/issuer-postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Postgres-backed IPointLedger implementation for @pafi-dev/issuer — TypeORM entities + migrations + base service issuers can drop in directly",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Pacific-Finance-Lab/pafi-sdk.git",
|
|
8
|
+
"directory": "packages/issuer-postgres"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Pacific-Finance-Lab/pafi-sdk/tree/main/packages/issuer-postgres#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Pacific-Finance-Lab/pafi-sdk/issues"
|
|
13
|
+
},
|
|
5
14
|
"type": "module",
|
|
6
15
|
"main": "./dist/index.cjs",
|
|
7
16
|
"module": "./dist/index.js",
|
|
@@ -42,25 +51,27 @@
|
|
|
42
51
|
"dist"
|
|
43
52
|
],
|
|
44
53
|
"dependencies": {
|
|
45
|
-
"@pafi-dev/issuer": "0.
|
|
54
|
+
"@pafi-dev/issuer": "0.29.0"
|
|
46
55
|
},
|
|
47
56
|
"peerDependencies": {
|
|
48
57
|
"typeorm": "^0.3.0",
|
|
49
58
|
"viem": "^2.0.0"
|
|
50
59
|
},
|
|
51
60
|
"devDependencies": {
|
|
61
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
62
|
+
"reflect-metadata": "^0.1.14",
|
|
52
63
|
"tsup": "^8.0.0",
|
|
53
|
-
"typescript": "^5.5.0",
|
|
54
64
|
"typeorm": "^0.3.20",
|
|
65
|
+
"typescript": "^5.5.0",
|
|
55
66
|
"viem": "^2.21.0",
|
|
56
|
-
"vitest": "^2.0.0"
|
|
57
|
-
"reflect-metadata": "^0.1.14"
|
|
67
|
+
"vitest": "^2.0.0"
|
|
58
68
|
},
|
|
59
69
|
"license": "Apache-2.0",
|
|
60
70
|
"scripts": {
|
|
61
71
|
"build": "tsup",
|
|
62
72
|
"test": "vitest run --passWithNoTests",
|
|
63
73
|
"test:watch": "vitest",
|
|
74
|
+
"test:cov": "vitest run --coverage --passWithNoTests",
|
|
64
75
|
"typecheck": "tsc --noEmit"
|
|
65
76
|
}
|
|
66
77
|
}
|