@pafi-dev/issuer-postgres 0.1.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 +178 -0
- package/dist/entities/index.cjs +303 -0
- package/dist/entities/index.cjs.map +1 -0
- package/dist/entities/index.d.cts +122 -0
- package/dist/entities/index.d.ts +122 -0
- package/dist/entities/index.js +292 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/index.cjs +764 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +97 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +749 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.cjs +138 -0
- package/dist/migrations/index.cjs.map +1 -0
- package/dist/migrations/index.d.cts +41 -0
- package/dist/migrations/index.d.ts +41 -0
- package/dist/migrations/index.js +110 -0
- package/dist/migrations/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
20
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
21
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
22
|
+
if (decorator = decorators[i])
|
|
23
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
24
|
+
if (kind && result) __defProp(target, key, result);
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/index.ts
|
|
29
|
+
var src_exports = {};
|
|
30
|
+
__export(src_exports, {
|
|
31
|
+
IndexerCursorEntity: () => IndexerCursorEntity,
|
|
32
|
+
InitialSchema1700000000000: () => InitialSchema1700000000000,
|
|
33
|
+
LedgerJournalEntity: () => LedgerJournalEntity,
|
|
34
|
+
LockedMintEntity: () => LockedMintEntity,
|
|
35
|
+
PAFI_ENTITIES: () => PAFI_ENTITIES,
|
|
36
|
+
PAFI_MIGRATIONS: () => PAFI_MIGRATIONS,
|
|
37
|
+
PendingCreditEntity: () => PendingCreditEntity,
|
|
38
|
+
PostgresCursorStore: () => PostgresCursorStore,
|
|
39
|
+
PostgresPointLedger: () => PostgresPointLedger,
|
|
40
|
+
UserBalanceEntity: () => UserBalanceEntity
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(src_exports);
|
|
43
|
+
|
|
44
|
+
// src/postgresPointLedger.ts
|
|
45
|
+
var import_viem = require("viem");
|
|
46
|
+
|
|
47
|
+
// src/entities/locked-mint.entity.ts
|
|
48
|
+
var import_typeorm = require("typeorm");
|
|
49
|
+
var LockedMintEntity = class {
|
|
50
|
+
id;
|
|
51
|
+
userAddress;
|
|
52
|
+
tokenAddress;
|
|
53
|
+
amount;
|
|
54
|
+
status;
|
|
55
|
+
createdAt;
|
|
56
|
+
expiresAt;
|
|
57
|
+
txHash;
|
|
58
|
+
userOpHash;
|
|
59
|
+
};
|
|
60
|
+
__decorateClass([
|
|
61
|
+
(0, import_typeorm.PrimaryGeneratedColumn)("uuid")
|
|
62
|
+
], LockedMintEntity.prototype, "id", 2);
|
|
63
|
+
__decorateClass([
|
|
64
|
+
(0, import_typeorm.Column)({ name: "user_address", type: "varchar", length: 42 })
|
|
65
|
+
], LockedMintEntity.prototype, "userAddress", 2);
|
|
66
|
+
__decorateClass([
|
|
67
|
+
(0, import_typeorm.Column)({ name: "token_address", type: "varchar", length: 42 })
|
|
68
|
+
], LockedMintEntity.prototype, "tokenAddress", 2);
|
|
69
|
+
__decorateClass([
|
|
70
|
+
(0, import_typeorm.Column)({
|
|
71
|
+
name: "amount",
|
|
72
|
+
type: "numeric",
|
|
73
|
+
precision: 78,
|
|
74
|
+
scale: 0,
|
|
75
|
+
transformer: {
|
|
76
|
+
to: (value) => value.toString(),
|
|
77
|
+
from: (value) => BigInt(value)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
], LockedMintEntity.prototype, "amount", 2);
|
|
81
|
+
__decorateClass([
|
|
82
|
+
(0, import_typeorm.Column)({
|
|
83
|
+
name: "status",
|
|
84
|
+
type: "varchar",
|
|
85
|
+
length: 16,
|
|
86
|
+
default: "PENDING"
|
|
87
|
+
})
|
|
88
|
+
], LockedMintEntity.prototype, "status", 2);
|
|
89
|
+
__decorateClass([
|
|
90
|
+
(0, import_typeorm.CreateDateColumn)({ name: "created_at" })
|
|
91
|
+
], LockedMintEntity.prototype, "createdAt", 2);
|
|
92
|
+
__decorateClass([
|
|
93
|
+
(0, import_typeorm.Column)({ name: "expires_at", type: "timestamp with time zone" })
|
|
94
|
+
], LockedMintEntity.prototype, "expiresAt", 2);
|
|
95
|
+
__decorateClass([
|
|
96
|
+
(0, import_typeorm.Column)({ name: "tx_hash", type: "varchar", length: 66, nullable: true })
|
|
97
|
+
], LockedMintEntity.prototype, "txHash", 2);
|
|
98
|
+
__decorateClass([
|
|
99
|
+
(0, import_typeorm.Column)({
|
|
100
|
+
name: "user_op_hash",
|
|
101
|
+
type: "varchar",
|
|
102
|
+
length: 66,
|
|
103
|
+
nullable: true
|
|
104
|
+
})
|
|
105
|
+
], LockedMintEntity.prototype, "userOpHash", 2);
|
|
106
|
+
LockedMintEntity = __decorateClass([
|
|
107
|
+
(0, import_typeorm.Entity)({ name: "locked_mint_requests" }),
|
|
108
|
+
(0, import_typeorm.Index)(["userAddress", "status"])
|
|
109
|
+
], LockedMintEntity);
|
|
110
|
+
|
|
111
|
+
// src/entities/pending-credit.entity.ts
|
|
112
|
+
var import_typeorm2 = require("typeorm");
|
|
113
|
+
var PendingCreditEntity = class {
|
|
114
|
+
id;
|
|
115
|
+
userAddress;
|
|
116
|
+
tokenAddress;
|
|
117
|
+
amount;
|
|
118
|
+
status;
|
|
119
|
+
txHash;
|
|
120
|
+
userOpHash;
|
|
121
|
+
createdAt;
|
|
122
|
+
expiresAt;
|
|
123
|
+
resolvedAt;
|
|
124
|
+
};
|
|
125
|
+
__decorateClass([
|
|
126
|
+
(0, import_typeorm2.PrimaryGeneratedColumn)("uuid")
|
|
127
|
+
], PendingCreditEntity.prototype, "id", 2);
|
|
128
|
+
__decorateClass([
|
|
129
|
+
(0, import_typeorm2.Column)({ name: "user_address", type: "varchar", length: 42 })
|
|
130
|
+
], PendingCreditEntity.prototype, "userAddress", 2);
|
|
131
|
+
__decorateClass([
|
|
132
|
+
(0, import_typeorm2.Column)({ name: "token_address", type: "varchar", length: 42 })
|
|
133
|
+
], PendingCreditEntity.prototype, "tokenAddress", 2);
|
|
134
|
+
__decorateClass([
|
|
135
|
+
(0, import_typeorm2.Column)({
|
|
136
|
+
name: "amount",
|
|
137
|
+
type: "numeric",
|
|
138
|
+
precision: 78,
|
|
139
|
+
scale: 0,
|
|
140
|
+
transformer: {
|
|
141
|
+
to: (value) => value.toString(),
|
|
142
|
+
from: (value) => BigInt(value)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
], PendingCreditEntity.prototype, "amount", 2);
|
|
146
|
+
__decorateClass([
|
|
147
|
+
(0, import_typeorm2.Column)({
|
|
148
|
+
name: "status",
|
|
149
|
+
type: "varchar",
|
|
150
|
+
length: 16,
|
|
151
|
+
default: "PENDING"
|
|
152
|
+
})
|
|
153
|
+
], PendingCreditEntity.prototype, "status", 2);
|
|
154
|
+
__decorateClass([
|
|
155
|
+
(0, import_typeorm2.Column)({ name: "tx_hash", type: "varchar", length: 66, nullable: true })
|
|
156
|
+
], PendingCreditEntity.prototype, "txHash", 2);
|
|
157
|
+
__decorateClass([
|
|
158
|
+
(0, import_typeorm2.Column)({
|
|
159
|
+
name: "user_op_hash",
|
|
160
|
+
type: "varchar",
|
|
161
|
+
length: 66,
|
|
162
|
+
nullable: true
|
|
163
|
+
})
|
|
164
|
+
], PendingCreditEntity.prototype, "userOpHash", 2);
|
|
165
|
+
__decorateClass([
|
|
166
|
+
(0, import_typeorm2.CreateDateColumn)({ name: "created_at" })
|
|
167
|
+
], PendingCreditEntity.prototype, "createdAt", 2);
|
|
168
|
+
__decorateClass([
|
|
169
|
+
(0, import_typeorm2.Column)({ name: "expires_at", type: "timestamp with time zone" })
|
|
170
|
+
], PendingCreditEntity.prototype, "expiresAt", 2);
|
|
171
|
+
__decorateClass([
|
|
172
|
+
(0, import_typeorm2.Column)({
|
|
173
|
+
name: "resolved_at",
|
|
174
|
+
type: "timestamp with time zone",
|
|
175
|
+
nullable: true
|
|
176
|
+
})
|
|
177
|
+
], PendingCreditEntity.prototype, "resolvedAt", 2);
|
|
178
|
+
PendingCreditEntity = __decorateClass([
|
|
179
|
+
(0, import_typeorm2.Entity)({ name: "pending_credits" }),
|
|
180
|
+
(0, import_typeorm2.Index)(["userAddress", "status"]),
|
|
181
|
+
(0, import_typeorm2.Index)(["txHash"])
|
|
182
|
+
], PendingCreditEntity);
|
|
183
|
+
|
|
184
|
+
// src/entities/user-balance.entity.ts
|
|
185
|
+
var import_typeorm3 = require("typeorm");
|
|
186
|
+
var UserBalanceEntity = class {
|
|
187
|
+
userAddress;
|
|
188
|
+
tokenAddress;
|
|
189
|
+
balance;
|
|
190
|
+
updatedAt;
|
|
191
|
+
};
|
|
192
|
+
__decorateClass([
|
|
193
|
+
(0, import_typeorm3.PrimaryColumn)({ name: "user_address", type: "varchar", length: 42 })
|
|
194
|
+
], UserBalanceEntity.prototype, "userAddress", 2);
|
|
195
|
+
__decorateClass([
|
|
196
|
+
(0, import_typeorm3.PrimaryColumn)({ name: "token_address", type: "varchar", length: 42 })
|
|
197
|
+
], UserBalanceEntity.prototype, "tokenAddress", 2);
|
|
198
|
+
__decorateClass([
|
|
199
|
+
(0, import_typeorm3.Column)({
|
|
200
|
+
name: "balance",
|
|
201
|
+
type: "numeric",
|
|
202
|
+
precision: 78,
|
|
203
|
+
scale: 0,
|
|
204
|
+
default: 0,
|
|
205
|
+
transformer: {
|
|
206
|
+
to: (value) => value.toString(),
|
|
207
|
+
from: (value) => BigInt(value)
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
], UserBalanceEntity.prototype, "balance", 2);
|
|
211
|
+
__decorateClass([
|
|
212
|
+
(0, import_typeorm3.UpdateDateColumn)({ name: "updated_at" })
|
|
213
|
+
], UserBalanceEntity.prototype, "updatedAt", 2);
|
|
214
|
+
UserBalanceEntity = __decorateClass([
|
|
215
|
+
(0, import_typeorm3.Entity)({ name: "user_balances" })
|
|
216
|
+
], UserBalanceEntity);
|
|
217
|
+
|
|
218
|
+
// src/entities/ledger-journal.entity.ts
|
|
219
|
+
var import_typeorm4 = require("typeorm");
|
|
220
|
+
var LedgerJournalEntity = class {
|
|
221
|
+
id;
|
|
222
|
+
userAddress;
|
|
223
|
+
tokenAddress;
|
|
224
|
+
delta;
|
|
225
|
+
reason;
|
|
226
|
+
txHash;
|
|
227
|
+
createdAt;
|
|
228
|
+
};
|
|
229
|
+
__decorateClass([
|
|
230
|
+
(0, import_typeorm4.PrimaryGeneratedColumn)("uuid")
|
|
231
|
+
], LedgerJournalEntity.prototype, "id", 2);
|
|
232
|
+
__decorateClass([
|
|
233
|
+
(0, import_typeorm4.Column)({ name: "user_address", type: "varchar", length: 42 })
|
|
234
|
+
], LedgerJournalEntity.prototype, "userAddress", 2);
|
|
235
|
+
__decorateClass([
|
|
236
|
+
(0, import_typeorm4.Column)({ name: "token_address", type: "varchar", length: 42 })
|
|
237
|
+
], LedgerJournalEntity.prototype, "tokenAddress", 2);
|
|
238
|
+
__decorateClass([
|
|
239
|
+
(0, import_typeorm4.Column)({
|
|
240
|
+
name: "delta",
|
|
241
|
+
type: "numeric",
|
|
242
|
+
precision: 78,
|
|
243
|
+
scale: 0,
|
|
244
|
+
transformer: {
|
|
245
|
+
to: (value) => value.toString(),
|
|
246
|
+
from: (value) => BigInt(value)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
], LedgerJournalEntity.prototype, "delta", 2);
|
|
250
|
+
__decorateClass([
|
|
251
|
+
(0, import_typeorm4.Column)({ name: "reason", type: "varchar", length: 128 })
|
|
252
|
+
], LedgerJournalEntity.prototype, "reason", 2);
|
|
253
|
+
__decorateClass([
|
|
254
|
+
(0, import_typeorm4.Column)({ name: "tx_hash", type: "varchar", length: 66, nullable: true })
|
|
255
|
+
], LedgerJournalEntity.prototype, "txHash", 2);
|
|
256
|
+
__decorateClass([
|
|
257
|
+
(0, import_typeorm4.CreateDateColumn)({ name: "created_at" })
|
|
258
|
+
], LedgerJournalEntity.prototype, "createdAt", 2);
|
|
259
|
+
LedgerJournalEntity = __decorateClass([
|
|
260
|
+
(0, import_typeorm4.Entity)({ name: "ledger_journal" }),
|
|
261
|
+
(0, import_typeorm4.Index)(["userAddress", "createdAt"])
|
|
262
|
+
], LedgerJournalEntity);
|
|
263
|
+
|
|
264
|
+
// src/postgresPointLedger.ts
|
|
265
|
+
var PostgresPointLedger = class {
|
|
266
|
+
constructor(dataSource, options = {}) {
|
|
267
|
+
this.dataSource = dataSource;
|
|
268
|
+
this.logger = options.logger;
|
|
269
|
+
}
|
|
270
|
+
dataSource;
|
|
271
|
+
logger;
|
|
272
|
+
// ---------------------------------------------------------------------
|
|
273
|
+
// Read
|
|
274
|
+
// ---------------------------------------------------------------------
|
|
275
|
+
async getBalance(userAddress, tokenAddress) {
|
|
276
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
277
|
+
await this.dataSource.getRepository(LockedMintEntity).createQueryBuilder().update().set({ status: "EXPIRED" }).where("status = :pending", { pending: "PENDING" }).andWhere("expires_at <= :now", { now: /* @__PURE__ */ new Date() }).execute();
|
|
278
|
+
const balanceRow = await this.dataSource.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
|
|
279
|
+
const total = balanceRow?.balance ?? 0n;
|
|
280
|
+
const locked = await this.sumPendingLocks(user, token);
|
|
281
|
+
const available = total - locked;
|
|
282
|
+
return available < 0n ? 0n : available;
|
|
283
|
+
}
|
|
284
|
+
async getLockedRequests(userAddress, tokenAddress) {
|
|
285
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
286
|
+
const rows = await this.dataSource.getRepository(LockedMintEntity).find({
|
|
287
|
+
where: { userAddress: user, tokenAddress: token, status: "PENDING" },
|
|
288
|
+
order: { createdAt: "ASC" }
|
|
289
|
+
});
|
|
290
|
+
return rows.map((row) => this.toSdkLock(row));
|
|
291
|
+
}
|
|
292
|
+
async getMintLock(lockId, userAddress) {
|
|
293
|
+
const row = await this.dataSource.getRepository(LockedMintEntity).findOne({ where: { id: lockId } });
|
|
294
|
+
if (!row) return null;
|
|
295
|
+
if (userAddress && row.userAddress.toLowerCase() !== userAddress.toLowerCase()) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
return this.toSdkLock(row);
|
|
299
|
+
}
|
|
300
|
+
/** Raw TypeORM row — escape hatch for callers that need entity fields. */
|
|
301
|
+
async getMintLockEntity(lockId) {
|
|
302
|
+
return this.dataSource.getRepository(LockedMintEntity).findOne({ where: { id: lockId } });
|
|
303
|
+
}
|
|
304
|
+
async getPendingCredit(lockId, userAddress) {
|
|
305
|
+
const row = await this.dataSource.getRepository(PendingCreditEntity).findOne({ where: { id: lockId } });
|
|
306
|
+
if (!row) return null;
|
|
307
|
+
if (userAddress && row.userAddress.toLowerCase() !== userAddress.toLowerCase()) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
lockId: row.id,
|
|
312
|
+
userAddress: (0, import_viem.getAddress)(row.userAddress),
|
|
313
|
+
amount: row.amount,
|
|
314
|
+
tokenAddress: row.tokenAddress ? (0, import_viem.getAddress)(row.tokenAddress) : void 0,
|
|
315
|
+
status: row.status,
|
|
316
|
+
createdAt: row.createdAt.getTime(),
|
|
317
|
+
expiresAt: row.expiresAt.getTime(),
|
|
318
|
+
txHash: row.txHash ?? void 0,
|
|
319
|
+
resolvedAt: row.resolvedAt?.getTime(),
|
|
320
|
+
userOpHash: row.userOpHash ?? void 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** Raw TypeORM row escape hatch for credits. */
|
|
324
|
+
async getPendingCreditEntity(lockId) {
|
|
325
|
+
return this.dataSource.getRepository(PendingCreditEntity).findOne({ where: { id: lockId } });
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Paginated list of a user's mint requests across all statuses and
|
|
329
|
+
* all tokens — used by `GET /user/transactions` reference endpoint.
|
|
330
|
+
*/
|
|
331
|
+
async listUserTransactions(userAddress, limit, offset) {
|
|
332
|
+
const user = (0, import_viem.getAddress)(userAddress);
|
|
333
|
+
const [rows, total] = await this.dataSource.getRepository(LockedMintEntity).findAndCount({
|
|
334
|
+
where: { userAddress: user },
|
|
335
|
+
order: { createdAt: "DESC" },
|
|
336
|
+
take: limit,
|
|
337
|
+
skip: offset
|
|
338
|
+
});
|
|
339
|
+
return { rows, total };
|
|
340
|
+
}
|
|
341
|
+
// ---------------------------------------------------------------------
|
|
342
|
+
// Write
|
|
343
|
+
// ---------------------------------------------------------------------
|
|
344
|
+
async creditBalance(userAddress, amount, reason, tokenAddress) {
|
|
345
|
+
if (amount <= 0n) {
|
|
346
|
+
throw new Error("creditBalance: amount must be positive");
|
|
347
|
+
}
|
|
348
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
349
|
+
await this.dataSource.transaction(async (tx) => {
|
|
350
|
+
const existing = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
|
|
351
|
+
const next = (existing?.balance ?? 0n) + amount;
|
|
352
|
+
await tx.getRepository(UserBalanceEntity).upsert(
|
|
353
|
+
{ userAddress: user, tokenAddress: token, balance: next },
|
|
354
|
+
{ conflictPaths: ["userAddress", "tokenAddress"] }
|
|
355
|
+
);
|
|
356
|
+
await tx.getRepository(LedgerJournalEntity).insert({
|
|
357
|
+
userAddress: user,
|
|
358
|
+
tokenAddress: token,
|
|
359
|
+
delta: amount,
|
|
360
|
+
reason
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
this.logger?.debug?.(`credit ${user}[${token}] +${amount} (${reason})`);
|
|
364
|
+
}
|
|
365
|
+
async lockForMinting(userAddress, amount, lockDurationMs, tokenAddress) {
|
|
366
|
+
if (amount <= 0n) {
|
|
367
|
+
throw new Error("lockForMinting: amount must be positive");
|
|
368
|
+
}
|
|
369
|
+
if (lockDurationMs <= 0) {
|
|
370
|
+
throw new Error("lockForMinting: lockDurationMs must be positive");
|
|
371
|
+
}
|
|
372
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
373
|
+
return this.dataSource.transaction(async (tx) => {
|
|
374
|
+
const balanceRow = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
|
|
375
|
+
const total = balanceRow?.balance ?? 0n;
|
|
376
|
+
const pendingTotal = await tx.getRepository(LockedMintEntity).createQueryBuilder("lock").select("COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)", "sum").where("lock.user_address = :user", { user }).andWhere("lock.token_address = :token", { token }).andWhere("lock.status = :pending", { pending: "PENDING" }).andWhere("lock.expires_at > :now", { now: /* @__PURE__ */ new Date() }).getRawOne();
|
|
377
|
+
const locked = pendingTotal ? BigInt(pendingTotal.sum) : 0n;
|
|
378
|
+
const available = total - locked;
|
|
379
|
+
if (available < amount) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Insufficient balance: available=${available}, requested=${amount}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const lock = await tx.getRepository(LockedMintEntity).save({
|
|
385
|
+
userAddress: user,
|
|
386
|
+
tokenAddress: token,
|
|
387
|
+
amount,
|
|
388
|
+
status: "PENDING",
|
|
389
|
+
expiresAt: new Date(Date.now() + lockDurationMs)
|
|
390
|
+
});
|
|
391
|
+
this.logger?.debug?.(
|
|
392
|
+
`lock ${lock.id} ${user}[${token}] amount=${amount}`
|
|
393
|
+
);
|
|
394
|
+
return lock.id;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async releaseLock(lockId) {
|
|
398
|
+
const result = await this.dataSource.getRepository(LockedMintEntity).delete({ id: lockId, status: "PENDING" });
|
|
399
|
+
if ((result.affected ?? 0) > 0) {
|
|
400
|
+
this.logger?.debug?.(`release lock ${lockId}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async deductBalance(userAddress, amount, txHash, tokenAddress) {
|
|
404
|
+
if (amount <= 0n) {
|
|
405
|
+
throw new Error("deductBalance: amount must be positive");
|
|
406
|
+
}
|
|
407
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
408
|
+
await this.dataSource.transaction(async (tx) => {
|
|
409
|
+
const balance = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
|
|
410
|
+
if (!balance || balance.balance < amount) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Cannot deduct ${amount} from balance ${balance?.balance ?? 0n}`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
await tx.getRepository(UserBalanceEntity).update(
|
|
416
|
+
{ userAddress: user, tokenAddress: token },
|
|
417
|
+
{ balance: balance.balance - amount }
|
|
418
|
+
);
|
|
419
|
+
await tx.getRepository(LedgerJournalEntity).insert({
|
|
420
|
+
userAddress: user,
|
|
421
|
+
tokenAddress: token,
|
|
422
|
+
delta: -amount,
|
|
423
|
+
reason: "MINT_CONFIRMED",
|
|
424
|
+
txHash
|
|
425
|
+
});
|
|
426
|
+
const match = await tx.getRepository(LockedMintEntity).findOne({
|
|
427
|
+
where: {
|
|
428
|
+
userAddress: user,
|
|
429
|
+
tokenAddress: token,
|
|
430
|
+
amount,
|
|
431
|
+
status: "PENDING"
|
|
432
|
+
},
|
|
433
|
+
order: { createdAt: "ASC" }
|
|
434
|
+
});
|
|
435
|
+
if (match) {
|
|
436
|
+
await tx.getRepository(LockedMintEntity).update({ id: match.id }, { status: "MINTED", txHash });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
this.logger?.log?.(`deduct ${user}[${token}] -${amount} tx=${txHash}`);
|
|
440
|
+
}
|
|
441
|
+
async updateMintStatus(lockId, status, txHash) {
|
|
442
|
+
const update = { status };
|
|
443
|
+
if (txHash) update.txHash = txHash;
|
|
444
|
+
await this.dataSource.getRepository(LockedMintEntity).update({ id: lockId }, update);
|
|
445
|
+
}
|
|
446
|
+
async bindMintUserOpHash(lockId, userOpHash) {
|
|
447
|
+
await this.dataSource.getRepository(LockedMintEntity).update({ id: lockId }, { userOpHash });
|
|
448
|
+
}
|
|
449
|
+
async bindCreditUserOpHash(lockId, userOpHash) {
|
|
450
|
+
await this.dataSource.getRepository(PendingCreditEntity).update({ id: lockId }, { userOpHash });
|
|
451
|
+
}
|
|
452
|
+
// ---------------------------------------------------------------------
|
|
453
|
+
// Reverse flow (burn → off-chain credit)
|
|
454
|
+
// ---------------------------------------------------------------------
|
|
455
|
+
async reservePendingCredit(userAddress, amount, durationMs, tokenAddress) {
|
|
456
|
+
if (amount <= 0n) {
|
|
457
|
+
throw new Error("reservePendingCredit: amount must be positive");
|
|
458
|
+
}
|
|
459
|
+
if (durationMs <= 0) {
|
|
460
|
+
throw new Error("reservePendingCredit: durationMs must be positive");
|
|
461
|
+
}
|
|
462
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
463
|
+
const row = await this.dataSource.getRepository(PendingCreditEntity).save({
|
|
464
|
+
userAddress: user,
|
|
465
|
+
tokenAddress: token,
|
|
466
|
+
amount,
|
|
467
|
+
status: "PENDING",
|
|
468
|
+
expiresAt: new Date(Date.now() + durationMs)
|
|
469
|
+
});
|
|
470
|
+
this.logger?.debug?.(
|
|
471
|
+
`reserve pending credit ${row.id} ${user}[${token}] +${amount}`
|
|
472
|
+
);
|
|
473
|
+
return row.id;
|
|
474
|
+
}
|
|
475
|
+
async resolveCreditByBurnTx(lockId, txHash) {
|
|
476
|
+
await this.dataSource.transaction(async (tx) => {
|
|
477
|
+
const credit = await tx.getRepository(PendingCreditEntity).findOne({ where: { id: lockId } });
|
|
478
|
+
if (!credit) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`resolveCreditByBurnTx: unknown pending credit ${lockId}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
if (credit.status === "RESOLVED") {
|
|
484
|
+
if (credit.txHash === txHash) return;
|
|
485
|
+
throw new Error(
|
|
486
|
+
`resolveCreditByBurnTx: credit ${lockId} already resolved with a different txHash`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (credit.status === "EXPIRED") {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`resolveCreditByBurnTx: credit ${lockId} already expired \u2014 burn landed too late`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
const user = credit.userAddress;
|
|
495
|
+
const token = credit.tokenAddress;
|
|
496
|
+
const alreadyResolved = await tx.getRepository(PendingCreditEntity).findOne({
|
|
497
|
+
where: {
|
|
498
|
+
userAddress: user,
|
|
499
|
+
tokenAddress: token,
|
|
500
|
+
txHash,
|
|
501
|
+
status: "RESOLVED"
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
if (alreadyResolved) {
|
|
505
|
+
await tx.getRepository(PendingCreditEntity).update(
|
|
506
|
+
{ id: lockId },
|
|
507
|
+
{ status: "RESOLVED", txHash, resolvedAt: /* @__PURE__ */ new Date() }
|
|
508
|
+
);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const balance = await tx.getRepository(UserBalanceEntity).findOne({ where: { userAddress: user, tokenAddress: token } });
|
|
512
|
+
const next = (balance?.balance ?? 0n) + credit.amount;
|
|
513
|
+
await tx.getRepository(UserBalanceEntity).upsert(
|
|
514
|
+
{ userAddress: user, tokenAddress: token, balance: next },
|
|
515
|
+
{ conflictPaths: ["userAddress", "tokenAddress"] }
|
|
516
|
+
);
|
|
517
|
+
await tx.getRepository(LedgerJournalEntity).insert({
|
|
518
|
+
userAddress: user,
|
|
519
|
+
tokenAddress: token,
|
|
520
|
+
delta: credit.amount,
|
|
521
|
+
reason: "BURN_FOR_CREDIT",
|
|
522
|
+
txHash
|
|
523
|
+
});
|
|
524
|
+
await tx.getRepository(PendingCreditEntity).update(
|
|
525
|
+
{ id: lockId },
|
|
526
|
+
{ status: "RESOLVED", txHash, resolvedAt: /* @__PURE__ */ new Date() }
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
this.logger?.log?.(`resolve pending credit ${lockId} tx=${txHash}`);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Used by `BurnIndexer.matchLockId` to resolve an on-chain burn
|
|
533
|
+
* event back to a pending credit row. Returns the oldest matching
|
|
534
|
+
* `(user, token, amount, status: PENDING)` lockId, or undefined
|
|
535
|
+
* when no match exists (unsolicited burn — indexer skips).
|
|
536
|
+
*/
|
|
537
|
+
async findPendingCreditLockId(userAddress, amount, tokenAddress) {
|
|
538
|
+
const { user, token } = normalize(userAddress, tokenAddress);
|
|
539
|
+
const row = await this.dataSource.getRepository(PendingCreditEntity).findOne({
|
|
540
|
+
where: {
|
|
541
|
+
userAddress: user,
|
|
542
|
+
tokenAddress: token,
|
|
543
|
+
amount,
|
|
544
|
+
status: "PENDING"
|
|
545
|
+
},
|
|
546
|
+
order: { createdAt: "ASC" }
|
|
547
|
+
});
|
|
548
|
+
return row?.id;
|
|
549
|
+
}
|
|
550
|
+
// ---------------------------------------------------------------------
|
|
551
|
+
// Internals
|
|
552
|
+
// ---------------------------------------------------------------------
|
|
553
|
+
async sumPendingLocks(userAddress, tokenAddress) {
|
|
554
|
+
const row = await this.dataSource.getRepository(LockedMintEntity).createQueryBuilder("lock").select("COALESCE(SUM(CAST(lock.amount AS NUMERIC)), 0)", "sum").where("lock.user_address = :user", { user: userAddress }).andWhere("lock.token_address = :token", { token: tokenAddress }).andWhere("lock.status = :pending", { pending: "PENDING" }).andWhere("lock.expires_at > :now", { now: /* @__PURE__ */ new Date() }).getRawOne();
|
|
555
|
+
return row ? BigInt(row.sum) : 0n;
|
|
556
|
+
}
|
|
557
|
+
toSdkLock(row) {
|
|
558
|
+
const out = {
|
|
559
|
+
lockId: row.id,
|
|
560
|
+
userAddress: row.userAddress,
|
|
561
|
+
tokenAddress: row.tokenAddress,
|
|
562
|
+
amount: row.amount,
|
|
563
|
+
status: row.status,
|
|
564
|
+
createdAt: row.createdAt.getTime(),
|
|
565
|
+
expiresAt: row.expiresAt.getTime()
|
|
566
|
+
};
|
|
567
|
+
if (row.txHash) out.txHash = row.txHash;
|
|
568
|
+
if (row.userOpHash) out.userOpHash = row.userOpHash;
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
function normalize(userAddress, tokenAddress) {
|
|
573
|
+
if (!tokenAddress) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
"PostgresPointLedger: tokenAddress is required on every call (multi-token ledger)"
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
user: (0, import_viem.getAddress)(userAddress),
|
|
580
|
+
token: (0, import_viem.getAddress)(tokenAddress)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/entities/indexer-cursor.entity.ts
|
|
585
|
+
var import_typeorm5 = require("typeorm");
|
|
586
|
+
var IndexerCursorEntity = class {
|
|
587
|
+
id;
|
|
588
|
+
nextBlock;
|
|
589
|
+
updatedAt;
|
|
590
|
+
};
|
|
591
|
+
__decorateClass([
|
|
592
|
+
(0, import_typeorm5.PrimaryColumn)({ type: "varchar", length: 64 })
|
|
593
|
+
], IndexerCursorEntity.prototype, "id", 2);
|
|
594
|
+
__decorateClass([
|
|
595
|
+
(0, import_typeorm5.Column)({
|
|
596
|
+
name: "next_block",
|
|
597
|
+
type: "numeric",
|
|
598
|
+
precision: 78,
|
|
599
|
+
scale: 0,
|
|
600
|
+
transformer: {
|
|
601
|
+
to: (value) => value.toString(),
|
|
602
|
+
from: (value) => BigInt(value)
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
], IndexerCursorEntity.prototype, "nextBlock", 2);
|
|
606
|
+
__decorateClass([
|
|
607
|
+
(0, import_typeorm5.UpdateDateColumn)({ name: "updated_at" })
|
|
608
|
+
], IndexerCursorEntity.prototype, "updatedAt", 2);
|
|
609
|
+
IndexerCursorEntity = __decorateClass([
|
|
610
|
+
(0, import_typeorm5.Entity)({ name: "indexer_cursors" })
|
|
611
|
+
], IndexerCursorEntity);
|
|
612
|
+
|
|
613
|
+
// src/postgresCursorStore.ts
|
|
614
|
+
var PostgresCursorStore = class _PostgresCursorStore {
|
|
615
|
+
constructor(dataSource, cursorId = "default") {
|
|
616
|
+
this.dataSource = dataSource;
|
|
617
|
+
this.cursorId = cursorId;
|
|
618
|
+
}
|
|
619
|
+
dataSource;
|
|
620
|
+
cursorId;
|
|
621
|
+
async load() {
|
|
622
|
+
const row = await this.dataSource.getRepository(IndexerCursorEntity).findOne({ where: { id: this.cursorId } });
|
|
623
|
+
return row?.nextBlock;
|
|
624
|
+
}
|
|
625
|
+
async save(blockNumber) {
|
|
626
|
+
await this.dataSource.getRepository(IndexerCursorEntity).upsert(
|
|
627
|
+
{ id: this.cursorId, nextBlock: blockNumber },
|
|
628
|
+
{ conflictPaths: ["id"] }
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
/** Derived store keyed by a different `id` — for sibling indexers. */
|
|
632
|
+
forKey(cursorId) {
|
|
633
|
+
return new _PostgresCursorStore(this.dataSource, cursorId);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/entities/index.ts
|
|
638
|
+
var PAFI_ENTITIES = [
|
|
639
|
+
LockedMintEntity,
|
|
640
|
+
PendingCreditEntity,
|
|
641
|
+
UserBalanceEntity,
|
|
642
|
+
LedgerJournalEntity,
|
|
643
|
+
IndexerCursorEntity
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
// src/migrations/1700000000000-InitialSchema.ts
|
|
647
|
+
var InitialSchema1700000000000 = class {
|
|
648
|
+
name = "InitialSchema1700000000000";
|
|
649
|
+
async up(queryRunner) {
|
|
650
|
+
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "pgcrypto"`);
|
|
651
|
+
await queryRunner.query(`
|
|
652
|
+
CREATE TABLE "user_balances" (
|
|
653
|
+
"user_address" varchar(42) NOT NULL,
|
|
654
|
+
"token_address" varchar(42) NOT NULL,
|
|
655
|
+
"balance" numeric(78, 0) NOT NULL DEFAULT 0,
|
|
656
|
+
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
|
657
|
+
CONSTRAINT "PK_user_balances" PRIMARY KEY ("user_address", "token_address")
|
|
658
|
+
)
|
|
659
|
+
`);
|
|
660
|
+
await queryRunner.query(`
|
|
661
|
+
CREATE TABLE "locked_mint_requests" (
|
|
662
|
+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
663
|
+
"user_address" varchar(42) NOT NULL,
|
|
664
|
+
"token_address" varchar(42) NOT NULL,
|
|
665
|
+
"amount" numeric(78, 0) NOT NULL,
|
|
666
|
+
"status" varchar(16) NOT NULL DEFAULT 'PENDING',
|
|
667
|
+
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
|
668
|
+
"expires_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
669
|
+
"tx_hash" varchar(66),
|
|
670
|
+
"user_op_hash" varchar(66),
|
|
671
|
+
CONSTRAINT "PK_locked_mint_requests" PRIMARY KEY ("id")
|
|
672
|
+
)
|
|
673
|
+
`);
|
|
674
|
+
await queryRunner.query(`
|
|
675
|
+
CREATE INDEX "IDX_locked_mint_user_status"
|
|
676
|
+
ON "locked_mint_requests" ("user_address", "token_address", "status")
|
|
677
|
+
`);
|
|
678
|
+
await queryRunner.query(`
|
|
679
|
+
CREATE INDEX "IDX_locked_mint_user_op_hash"
|
|
680
|
+
ON "locked_mint_requests" ("user_op_hash")
|
|
681
|
+
`);
|
|
682
|
+
await queryRunner.query(`
|
|
683
|
+
CREATE TABLE "pending_credits" (
|
|
684
|
+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
685
|
+
"user_address" varchar(42) NOT NULL,
|
|
686
|
+
"token_address" varchar(42) NOT NULL,
|
|
687
|
+
"amount" numeric(78, 0) NOT NULL,
|
|
688
|
+
"status" varchar(16) NOT NULL DEFAULT 'PENDING',
|
|
689
|
+
"tx_hash" varchar(66),
|
|
690
|
+
"user_op_hash" varchar(66),
|
|
691
|
+
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
|
692
|
+
"expires_at" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
693
|
+
"resolved_at" TIMESTAMP WITH TIME ZONE,
|
|
694
|
+
CONSTRAINT "PK_pending_credits" PRIMARY KEY ("id")
|
|
695
|
+
)
|
|
696
|
+
`);
|
|
697
|
+
await queryRunner.query(`
|
|
698
|
+
CREATE INDEX "IDX_pending_credits_user_status"
|
|
699
|
+
ON "pending_credits" ("user_address", "token_address", "status")
|
|
700
|
+
`);
|
|
701
|
+
await queryRunner.query(`
|
|
702
|
+
CREATE INDEX "IDX_pending_credits_tx_hash"
|
|
703
|
+
ON "pending_credits" ("tx_hash")
|
|
704
|
+
`);
|
|
705
|
+
await queryRunner.query(`
|
|
706
|
+
CREATE INDEX "IDX_pending_credits_user_op_hash"
|
|
707
|
+
ON "pending_credits" ("user_op_hash")
|
|
708
|
+
`);
|
|
709
|
+
await queryRunner.query(`
|
|
710
|
+
CREATE TABLE "ledger_journal" (
|
|
711
|
+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
712
|
+
"user_address" varchar(42) NOT NULL,
|
|
713
|
+
"token_address" varchar(42) NOT NULL,
|
|
714
|
+
"delta" numeric(78, 0) NOT NULL,
|
|
715
|
+
"reason" varchar(128) NOT NULL,
|
|
716
|
+
"tx_hash" varchar(66),
|
|
717
|
+
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
|
718
|
+
CONSTRAINT "PK_ledger_journal" PRIMARY KEY ("id")
|
|
719
|
+
)
|
|
720
|
+
`);
|
|
721
|
+
await queryRunner.query(`
|
|
722
|
+
CREATE INDEX "IDX_ledger_journal_user_created"
|
|
723
|
+
ON "ledger_journal" ("user_address", "token_address", "created_at")
|
|
724
|
+
`);
|
|
725
|
+
await queryRunner.query(`
|
|
726
|
+
CREATE TABLE "indexer_cursors" (
|
|
727
|
+
"id" varchar(64) NOT NULL,
|
|
728
|
+
"next_block" numeric(78, 0) NOT NULL,
|
|
729
|
+
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
|
730
|
+
CONSTRAINT "PK_indexer_cursors" PRIMARY KEY ("id")
|
|
731
|
+
)
|
|
732
|
+
`);
|
|
733
|
+
}
|
|
734
|
+
async down(queryRunner) {
|
|
735
|
+
await queryRunner.query(`DROP TABLE "indexer_cursors"`);
|
|
736
|
+
await queryRunner.query(`DROP INDEX "IDX_ledger_journal_user_created"`);
|
|
737
|
+
await queryRunner.query(`DROP TABLE "ledger_journal"`);
|
|
738
|
+
await queryRunner.query(`DROP INDEX "IDX_pending_credits_user_op_hash"`);
|
|
739
|
+
await queryRunner.query(`DROP INDEX "IDX_pending_credits_tx_hash"`);
|
|
740
|
+
await queryRunner.query(`DROP INDEX "IDX_pending_credits_user_status"`);
|
|
741
|
+
await queryRunner.query(`DROP TABLE "pending_credits"`);
|
|
742
|
+
await queryRunner.query(`DROP INDEX "IDX_locked_mint_user_op_hash"`);
|
|
743
|
+
await queryRunner.query(`DROP INDEX "IDX_locked_mint_user_status"`);
|
|
744
|
+
await queryRunner.query(`DROP TABLE "locked_mint_requests"`);
|
|
745
|
+
await queryRunner.query(`DROP TABLE "user_balances"`);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/migrations/index.ts
|
|
750
|
+
var PAFI_MIGRATIONS = [InitialSchema1700000000000];
|
|
751
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
752
|
+
0 && (module.exports = {
|
|
753
|
+
IndexerCursorEntity,
|
|
754
|
+
InitialSchema1700000000000,
|
|
755
|
+
LedgerJournalEntity,
|
|
756
|
+
LockedMintEntity,
|
|
757
|
+
PAFI_ENTITIES,
|
|
758
|
+
PAFI_MIGRATIONS,
|
|
759
|
+
PendingCreditEntity,
|
|
760
|
+
PostgresCursorStore,
|
|
761
|
+
PostgresPointLedger,
|
|
762
|
+
UserBalanceEntity
|
|
763
|
+
});
|
|
764
|
+
//# sourceMappingURL=index.cjs.map
|